Alternatives to a wizard - python

I'm making a program that fits the wizard concept ideally; the user is walked through the steps to create a character for a game.
However, I'm realizing that the limitations of the wizard are making it difficult to design "elegant" logic flow. For example, because all pages of the wizard are initalized at the same time, I can't have the values entered in one page available to the next one. I have to put a button on each page to get the values from a previous page rather than simply having fields auto-populated.
I've thought about alternatives to using the wizard. I think the best idea is to have some buttons on one panel that change the information on another panel, e.g. a splitter window.
However, I can't find any documentation in wxPython on how to dynamically change the panel. Everything I've found so far is really pretty static, hence the use of the wizard. Even the "wxPython in Action" book doesn't mention it.
Are there any tutorials for making "dynamic panels" or better management of a wizard?

Here is a simple example. This way you can make your "wizard" work like a finite state machine where states are different pages that are initialized on demand. Also, the data is shared between pages.
import wx
import wx.lib.newevent
(PageChangeEvent, EVT_PAGE_CHANGE) = wx.lib.newevent.NewEvent()
class Data:
foo = None
bar = None
class Page1(wx.Panel):
def __init__(self, parent, data):
wx.Panel.__init__(self, parent)
self.parent = parent
self.data = data
sizer = wx.BoxSizer(wx.VERTICAL)
self.SetSizer(sizer)
label = wx.StaticText(self, label="Page 1 - foo")
self.foo = wx.TextCtrl(self)
goto_page2 = wx.Button(self, label="Go to page 2")
for c in (label, self.foo, goto_page2):
sizer.Add(c, 0, wx.TOP, 5)
goto_page2.Bind(wx.EVT_BUTTON, self.OnPage2)
def OnPage2(self, event):
self.data.foo = self.foo.Value
wx.PostEvent(self.parent, PageChangeEvent(page=Page2))
class Page2(wx.Panel):
def __init__(self, parent, data):
wx.Panel.__init__(self, parent)
self.parent = parent
self.data = data
sizer = wx.BoxSizer(wx.VERTICAL)
self.SetSizer(sizer)
label = wx.StaticText(self, label="Page 2 - bar")
self.bar = wx.TextCtrl(self)
goto_finish = wx.Button(self, label="Finish")
for c in (label, self.bar, goto_finish):
sizer.Add(c, 0, wx.TOP, 5)
goto_finish.Bind(wx.EVT_BUTTON, self.OnFinish)
def OnFinish(self, event):
self.data.bar = self.bar.Value
wx.PostEvent(self.parent, PageChangeEvent(page=finish))
def finish(parent, data):
wx.MessageBox("foo = %s\nbar = %s" % (data.foo, data.bar))
wx.GetApp().ExitMainLoop()
class Test(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, None)
self.data = Data()
self.current_page = None
self.Bind(EVT_PAGE_CHANGE, self.OnPageChange)
wx.PostEvent(self, PageChangeEvent(page=Page1))
def OnPageChange(self, event):
page = event.page(self, self.data)
if page == None:
return
if self.current_page:
self.current_page.Destroy()
self.current_page = page
page.Layout()
page.Fit()
page.Refresh()
app = wx.PySimpleApp()
app.TopWindow = Test()
app.TopWindow.Show()
app.MainLoop()

The wxPython demo has an example of a "dynamic" wizard. Pages override GetNext() and GetPrev() to show pages dynamically. This shows the basic technique; you can extend it to add and remove pages, change pages on the fly, and rearrange pages dynamically.
The wizard class is just a convenience, though. You can modify it, or create your own implementation. A style that seems popular nowadays is to use an HTML-based presentation; you can emulate this with the wxHtml control, or the IEHtmlWindow control if your app is Windows only.

You could try using a workflow engine like WFTK. In this particular case author has done some work on wx-based apps using WFTK and can probably direct you to examples.

I'd get rid of wizard in whole. They are the most unpleasant things I've ever used.
The problem that requires a wizard-application where you click 'next' is perhaps a problem where you could apply a better user interface in a bit different manner. Instead of bringing up a dialog with annoying 'next' -button. Do this:
Bring up a page. When the user inserts the information to the page, extend or shorten it according to the input. If your application needs to do some processing to continue, and it's impossible to revert after that, write a new page or disable the earlier section of the current page. When you don't need any input from the user anymore or the app is finished, you can show a button or enable an existing such.
I don't mean you should implement it all in browser. Make simply a scrolling container that can contain buttons and labels in a flat list.
Benefit: The user can just click a tab, and you are encouraged to put all the processing into the end of filling the page.

It should be noted that a Wizard should be the interface for mutli-step, infrequently-performed tasks. The wizard is used to guide the user through something they don't really understand, because they almost never do it.
And if some users might do the task frequently, you want to give those power users a lightweight interface to do the same thing - even if it less self explanatory.
See: Windows Vista User Experience Guidelines - Top Violations
Wizards
Consider lightweight alternatives first, such as dialog boxes, task
panes, or single pages. Wizards are
a heavy UI, best used for multi-step,
infrequently performed task. You don't
have to use wizards—you can provide
helpful information and assistance in
any UI.

Related

Python modules and binding to functions

I am a self-taught in python and I have been programming for the last year or so. Most of my knowledge has been attained from googling and trial and error. I apologize in advance if I am not using the proper terms so please correct me.
I am at a roadblock with my program, so I will post a few bits of relevant code.
I have a button (that references a function) that I want to use in several different classes. My button is one class and the function is in a different class.
When I start my program, it runs my def_init(self) and that runs my universal_windows function and my main window references the class button_position and the function make_buttons and the sub function button_template.
(There are more functions, but this is a snippet)
class button_position:
def make_buttons(self,panel,sizer,v_box_size,v_rt,
v_delete,v_move_up,v_move_down):
def button_template():
delete_clip = wx.Button(panel, label="Delete\n Clip",pos = (975,v_delete))
delete_clip.Bind(wx.EVT_BUTTON, self.delete_clips)
delete_clip.SetSize((48,45))
delete_clip.SetBackgroundColour("Red")
delete_clip.SetForegroundColour("Black")
# print(type(delete_clip))
(PROGRAM CONTINUES)
delete_clips is another function (located in the the class button_functions) and all of my buttons work in my main window when called by the init function.
I want to reuse this function in another window, so I call button_position in another class. The buttons appear fine but my program cannot reference the functions that are associated with button_position function. I have to make new functions (with the same name) and a reference to the original function to have those buttons work. Since I have to "redefine" the functions in every class I use the functions in, it means a lot of bloating in my classes. I have tried to just call them without defining and that doesn't work. Ultimately, I don't want to have to redfine them at all.
class Stringout_window(wx.Frame):
""""""
Functions to delete clips, move clips up, move clips down
def delete_clips(self,event):
button_functions.delete_clips(self,event)
def move_clips_up(self,event):
button_functions.move_clips_up(self,event)
def move_clips_down(self,event):
button_functions.move_clips_down(self,event)
def accel(self):
# Edit_window.accelerator_commands(self)
print("this")
...
def __init__(self):
pub.subscribe(self.my_listener2, "bottom_data")
# print(bottom_data)
self.accel()
wx.Frame.__init__(self, None, wx.ID_ANY, "Full Screen",style= wx.MINIMIZE_BOX
| wx.CLOSE_BOX,pos = (10,20),size = (1075,1150))
self.SetFont(wx.Font(13, wx.FONTFAMILY_DEFAULT, wx.NORMAL, wx.NORMAL))
# Add a panel so it looks the correct on all platforms
panel = wx.Panel(self, wx.ID_ANY)
self.index = 0
#Sizer placed at the top because the sizer attributes are part of each object.
sizer = wx.BoxSizer(wx.VERTICAL)
h_sizer = wx.BoxSizer(wx.HORIZONTAL)
#Sizer to add space to top
sizer.Add(0,60,0)
button_position.make_buttons(self,panel,sizer,982,20,
85,200,350)
(PROGRAM CONTINUES)
I thought of trying to reference the function in the button_template but that doesn't work properly either
def button_template():
delete_clip = wx.Button(panel, label="Delete\n Clip",pos = (975,v_delete))
delete_clip.Bind(wx.EVT_BUTTON, self.button_function.delete_clips)
Please let me know if you need more information.
Can someone give me a bit of advice? I appreciate your help.
Thanks

wxPython MVC working with GUI

I have a working program in 1 class. I wish to convert it to MVC pattern.
I've implemented observer design as shown here and i opened 3 .py files for Model, View and Controller.
I'm having trouble understanding what goes where.
For example:
this code in the original class
helpMenu = wx.Menu()
helpMenu.Append(ID_ABOUT, "&About", "Display info")
self.Bind(event=wx.EVT_MENU, handler=self.OnHelpAbout, id=ID_ABOUT)
...
def OnHelpAbout(self,e):
title = self.GetTitle()
d = wx.MessageDialog(self, "About " + title, title, wx.ICON_INFORMATION | wx.OK)
d.ShowModal()
d.Destroy()
This menu code lines should be in the View, and the function OnHelpAbout should be implemented in the Controler. But how do I call it? should I have an instance of the Controller in the View? Where do I sign for the observer?
Also, if this function set a dialog = making changes in the view How do I do that? If I understood correctly I need to call another function in the View and put the code there?
I guess what i'm missing is a basic template to work with. I haven't found any source that explains how to do it with GUI & GUI Events.
The MVC entry in the wxPython wiki gives a good insight how the spearation of concerns could look like. The first (newer) example leverages pypubsub, the older example at the bottom deals with wx.Events alone.
While it may or may not conform to the strict MVC paradigma, it shows beautifully how the model is separated form view and controller.
In your example the creation of the menu (the first two lines) would go into the view. The controller would hold the event method (OnHelpAbout) and do the binding on the menu item. For the model your example would have no code. The frame title could be kept in the controller and put into the frame on object creation/after creation.
UPDATE/EDIT1
This is no exact science and can get messy as already pointed out. How about the following:
view.py
class myframe(wx.Frame):
def __init__(…
# code for menu creation goes here
self.helpmenu = …
controller.py
from view import myframe
class mycontroller(object):
def __init__(self, app):
self.title = 'mytitle'
self.frame = myframe(…
self.frame.SetTitle(self.title)
self.frame.helpmenu.Bind(…, handler=self.OnHelpAbout, …)
self.frame.Show()
def OnHelpAbout(self, evt):
title = self.title
# show message dialog
…
if __name__ == '__main__':
app = wx.App(…
ctrler = mycontroller(app)
app.MainLoop()
Wake me if you manage to find a "pure" solution …

wxpython communication between notebook and main frame

I have a notebook whose parent is the main frame of the application. The main frame also has a panel showing a chart to the side of the notebook, a menu bar and a status bar.
The notebook has a couple of pages and each page has some nested panels.
I'd like the callbacks for buttons in those panels to be able to talk to the main frame.
At the moment, that means a ridiculous chain of 'parents'. For example, to get to status bar from a panel on a notebook page I would do:
stat = self.parent.parent.parent.status_bar
The first parent is the notebook page, the second parent is the notebook and finally the last parent is the main frame.
This leads to very obtuse code...
Naturally you can see how this might get worse if I wanted to talk between elements on the panel adjacent to the notebook or nest the notebook in it's own panel..
Any tips?
There is a simple way to get your Main Frame.
Since you can get your app instance anywhere in your code with "wx.GetApp()", then you can set your Mainframe into your app instance, it would be easy to fecth.
Please try following simple sample:
import wx
class TestFrame(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, None)
btn = wx.Button(wx.Panel(self), label = "test")
self.Bind(wx.EVT_BUTTON, self.onButton)
def onButton(self, evt):
print "onButton"
app = wx.GetApp()
print app.Myframe
app = wx.App()
frame = TestFrame()
frame.Center()
frame.Show()
app.Myframe = frame
app.MainLoop()
If you need to get access to the top frame, you should be able to use wx.GetTopLevelParent(). Personally, I think pubsub is probably the easiest way to call various classes in wxPython and it's pretty clean too. Plus if you need to call multiple frames or panels or whatever, you can have them all "subscribe" to the same message name and then publish a message for all of them to pick up.
Here's a tutorial for pubsub: http://www.blog.pythonlibrary.org/2013/09/05/wxpython-2-9-and-the-newer-pubsub-api-a-simple-tutorial/

Catch a key press using accelerator tables without preventing typing in text controls

I am trying to use an accelerator table to catch a key press globally in the window so that I can fire a method in response. However, the accelerator table seems to destroy the event that would otherwise be created by pressing the key, preventing me from typing it into text controls. Is there any way to get around this behavior, or some other solution to my problem?
Edit: Here's a better example of the kind of situation:
class MainWindow(wx.Frame):
def __init__(self, parent, title):
wx.Frame.__init__(self, parent, title=title, size=(-1,-1))
...
randomId = wx.NewId()
parent.Bind(wx.EVT_MENU, self.onKey, id=randomId)
accTable = wx.AcceleratorTable([(wx.ACCEL_NORMAL, 96, randomId)]) # 96 is the keycode for `
self.SetAcceleratorTable(accTable)
...
class ctrlpanel(wx.Panel):
def __init__(self, parent):
wx.Panel.__init__(self, parent)
...
grid = wx.GridBagSizer(hgap=7, vgap=7)
self.lblfreq = wx.StaticText(self, label="Base Frequency (hz): ")
grid.Add(self.lblfreq, pos=(0,0))
self.freq = wx.TextCtrl(self, value="0", size=(100, 20))
grid.Add(self.freq, pos=(0,1))
self.waveList = ['Sine', 'Semicircle', 'Saw', 'Square']
self.lblwave = wx.StaticText(self, label="Wave Shape: ")
grid.Add(self.lblwave, pos=(1,0))
self.wave = wx.Choice(self, choices=self.waveList)
self.wave.SetStringSelection('Sine')
grid.Add(self.wave, pos=(1,1))
self.SetSizerAndFit(grid)
With this code, the accelerator table works perfectly, picking up my key presses wherever the focus is on the window, and activating the onKey method. However, when the focus is on the text control, I can't type the character I'm picking up into the text control because the accelerator table is handling it the event before it goes anywhere else, or preventing the normal event from being fired or propagated. I want to be able to make the event still happen normally, so text will appear in the text control when typed, even if those keys are set to do some other action generally in the window. Is there any event.Skip() type thing I could use here?
It's hard to say what the issue is without more information, like OS, wx version or a small example that shows the problem. However, here are a couple example articles on the subject that might help you figure out the problem:
http://www.blog.pythonlibrary.org/2008/07/02/wxpython-working-with-menus-toolbars-and-accelerators/
http://www.blog.pythonlibrary.org/2010/12/02/wxpython-keyboard-shortcuts-accelerators/

wxPython, Set value of StaticText()

I am making a little GUI frontend for a app at the moment using wxPython.
I am using wx.StaticText() to create a place to hold some text, code below:
content = wx.StaticText(panel, -1, "Text Here", style=wx.ALIGN_CENTRE)
I have a button when clicked retrieves data from MySQL, I am wanting to change the value of the StaticText() to the MySQL data or what else could I use the hold the data.
I have tried using the below method:
contents = wx.TextCtrl(bkg, style=wx.TE_MULTILINE | wx.HSCROLL)
content.SetValue("New Text")
This displays the data fine but after the data is loaded you can edit the data and I do not want this.
Hope you guys understand what I am trying to do, I am new to Python :)
Cheers
If you are using a wx.StaticText() you can just:
def __init__(self, parent, *args, **kwargs): #frame constructor, etc.
self.some_text = wx.StaticText(panel, wx.ID_ANY, label="Awaiting MySQL Data", style=wx.ALIGN_CENTER)
def someFunction(self):
mysql_data = databasemodel.returnData() #query your database to return a string
self.some_text.SetLabel(mysql_data)
As litb mentioned, the wxWidgets docs are often much easier to use than the wxPython docs. In order to see that the SetLabel() function can be applied to a wx.StaticText instance, you have to travel up the namespace hierarchy in the wxPython docs to the wxWindow superclass, from which wx.StaticText is subclassed. There are a few things different in wxPython from wxWidgets, and it can be challenging to find out what they are. Fortunately, a lot of the time, the differences are convenience functions that have been added to wxPython and are not found in wxWidgets.
wx.TextCtrl has a style called wx.TE_READONLY . Use that to make it read-only.
As a sidenode, you can use the C++ wxWidgets Manual for wxPython aswell. Where special handling for wxPython or other ports is required, the manual often points out the difference.

Categories