Python Forum
[WxPython] [Tutorial] Notespad - Create a text editor - W.I.P.
Thread Rating:
  • 1 Vote(s) - 5 Average
  • 1
  • 2
  • 3
  • 4
  • 5
[WxPython] [Tutorial] Notespad - Create a text editor - W.I.P.
#1
NOTE: This is still W.I.P.
In this tutorial, I'm going to attempt to build a notespad in WxPython in stages and describe what each part of the code is doing.
Updated to work with python 3 I am using python version 2.7.3, WxPython version 2.9.4.0, and  windowsXp.

WxPython site link
Links to existing WxPython tutorials that are probably way better then mine  :lol:
http://wiki.wxpython.org/Getting%20Started
http://zetcode.com/wxpython/
Lets get started.
import wx  # 1
if __name__ == '__main__':  # 2
    wx_app = wx.App(False)  # 3
    wx_app.MainLoop()  # 4

  1. Imports the wxPython module.
  2. This means only run the following code when you run this module directly, if you import this module, the following code will not run.
  3. Every Wxpython application needs one wx.App object, the False parameter means don't redirect stdout and stderr to a window.
  4. The method call MainLoop on the App object starts an infinite loop that checks for 'events', all the while there is a wx window in existence.
    we will get to events later, running this code as it is, will just exit the loop as there is no window created yet.
Lets create a Frame so we can actually see something happen, Running this code we should now actually see a very basic window, the mainloop is now infinitely looping, by clicking on the frames X an event will be sent to the App telling it that this window has been closed and as there are no other windows in existence will also exit the mainloop.
import wx
if __name__ == '__main__':
    wx_app = wx.App(False)
    frame = wx.Frame(None)  # 1
    frame.Show()  # 2
    wx_app.MainLoop()

  1. To start with we create a wx.Frame, a Frame is a container object that we can add other wx objects to, calling None tells the frame that it has no parent.
  2. To make the frame visible we have to call its Show method.
Lets make it a little more interesting, rather then just using the basic wx Frame we'll make our own class of Frame and call it Notepad.
import wx

class Notespad(wx.Frame):  # 1
    def __init__(self, *args, **kwargs):  # 2
        super(Notespad, self).__init__(*args, **kwargs)  # 3
        self.SetTitle('Untitled - Notespad')  # 4
        panel = wx.Panel(self)  # 5
        sizer = wx.BoxSizer(wx.VERTICAL)  # 6
        sizer.Add(panel, 1, wx.EXPAND)  # 7
        self.SetSizer(sizer)  # 8
        self.Layout()  # 9

if __name__ == '__main__':
    wx_app = wx.App(False)
    frame = Notespad(None)  # 10
    frame.Show()
    wx_app.MainLoop()

  1. Creates a new class named Notespad which inherits from a wxFrame.
  2. Allows us to pass in any attributes.
  3. The attributes we passed in are given to the wxFrame.
  4. Sets the title of the frame.
  5. Creates a wxPanel object, this is a container object that sits on the frame as a background, we tell it that the wxFrame is its parent by giving it the attribute self.
  6. Creates a sizer, this is a kind of invisible container that automatically arranges its objects on the sizer's owner.
  7. Adds the panel we created to the sizer, the second attribute number 1 is the proportion of the of the space in the sizer it will take, and as we set the sizer to be a vertical that's the direction the proportion relates to, for instance if we was to add another object to the sizer with the same proportion, both objects would take up half of the available space each in the vertical direction. The third attribute wxExpand tells the sizer to stretch the object to take up the full width in the opposite direction, the Horizontal in this case.
  8. the wxFrame is told to let the sizer we created, stretch into the available space it has.
  9. Calling this  tells any sizer to arrange the objects there are controlling the position and size of.
  10. Now we are creating an instance of our own wxFrame and again passing in the attribute None to indicate that it has no parent
Its not a very good Notespad yet as we cant even type any text into it, lets fix that.
import wx

class Notespad(wx.Frame):
    def __init__(self, *args, **kwargs):
        super(Notespad, self).__init__(*args, **kwargs)
        self.SetTitle('Untitled - Notepad')
        panel = wx.Panel(self)
        txt_ctrl = wx.TextCtrl(panel, style=wx.TE_MULTILINE)  # 1
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(panel, 1, wx.EXPAND)
        p_sizer = wx.BoxSizer(wx.VERTICAL)  # 2
        p_sizer.Add(txt_ctrl, 1, wx.EXPAND)  # 3
        panel.SetSizer(p_sizer)  # 4
        self.SetSizer(sizer)
        self.Layout()

if __name__ == '__main__':
    wx_app = wx.App(False)
    frame = Notespad(None)
    frame.Show()
    wx_app.MainLoop()

  1. Creates a wxTextCtrl and set its parent as the panel the style parameter makes us able to type on more then just the first row
  2. Because the panel now has controls it needs a sizer to keep them under control just the the frame needed a sizer for the panel
  3. Adding the wxTextCtrl to the panels sizer with the same parameters used in the previous sizer's add method
  4. tells the panel to use this sizer
Reply
#2
We want to be able to open existing txt files and save any new ones so let get started on adding a Menubar and a Statusbar.
import wx

class Notespad(wx.Frame):
    def __init__(self, *args, **kwargs):
        super(Notespad, self).__init__(*args, **kwargs)
        self.SetTitle('Untitled - Notespad')
        self.CreateStatusBar()  # 1
        menubar = wx.MenuBar()  # 2
        self.SetMenuBar(menubar)  # 3
        file_menu = wx.Menu()  # 4
        menu_exit = file_menu.Append(-1, "Exit", "Exit the Application")  # 5
        menubar.Append(file_menu, "&File")  # 6
        panel = wx.Panel(self)
        txt_ctrl = wx.TextCtrl(panel, style=wx.TE_MULTILINE)
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(panel, 1, wx.EXPAND)
        p_sizer = wx.BoxSizer(wx.VERTICAL)
        p_sizer.Add(txt_ctrl, 1, wx.EXPAND)
        panel.SetSizer(p_sizer)
        self.SetSizer(sizer)
        self.Layout()
        self.Bind(wx.EVT_MENU, self.on_menu_exit, menu_exit)  # 7
    def on_menu_exit(self, event):  # 8
        self.Close()  # 9
        event.Skip()  # 10

if __name__ == '__main__':
    wx_app = wx.App(False)
    frame = Notespad(None)
    frame.Show()
    wx_app.MainLoop()

  1. Creates a statusbar that shows information about whats happening along the bottom of the frame.
  2. Creates an empty menubar.
  3. Adds the menubar to our frame.
  4. Creates an empty menubar item.
  5. Creates a menu item named 'Exit', the first parameter is for an ID but we don't need one here so just using -1, the second parameter is the text that will be displayed for this menu item, the third parameter is the text that will be shown in the statusbar when the mouse cursor is over this menu item.
  6. Adds the file_menu item to the menubar and set its display text as File, the '&' Makes the letter following it to be a keyboard shortcut to this menu item.
  7. Creates an event, the first parameter is the type of event, the second is what to call when this event happens and the third is which menu item of the menu this event applies to.
  8. This is our event call it must receive one argument.
  9. This tells our frame to close.
  10. This line tells the app to continue processing events of this type
Lets now add another menu item to enable us to open files
import wx

class Notespad(wx.Frame):
    def __init__(self, *args, **kwargs):
        super(Notespad, self).__init__(*args, **kwargs)
        self.SetTitle('Untitled - Notespad')
        self.CreateStatusBar()
        menubar = wx.MenuBar()
        self.SetMenuBar(menubar)
        file_menu = wx.Menu()
        menu_open = file_menu.Append(wx.ID_OPEN, "&Open",
                                     "Open an existing file")  # 1
        file_menu.AppendSeparator()  # 2
        menu_exit = file_menu.Append(-1, "Exit", "Exit the Application")
        menubar.Append(file_menu, "&File")
        panel = wx.Panel(self)
        self.txt_ctrl = wx.TextCtrl(panel, style=wx.TE_MULTILINE)  # 3
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(panel, 1, wx.EXPAND)
        p_sizer = wx.BoxSizer(wx.VERTICAL)
        p_sizer.Add(self.txt_ctrl, 1, wx.EXPAND)  # 4
        panel.SetSizer(p_sizer)
        self.SetSizer(sizer)
        self.Layout()
        self.Bind(wx.EVT_MENU, self.on_menu_exit, menu_exit)
        self.Bind(wx.EVT_MENU, self.on_menu_open, menu_open)  # 5
    def on_menu_exit(self, event):
        self.Close()
        event.Skip()
    def on_menu_open(self, event):  # 6
        self.file_open()  # 7
        event.Skip()  # 8
    def file_open(self):  # 9
        wildcard = "Text Documents (*.txt)|*.txt|Python Documents (*.py)|*.py"  # 10
        with wx.FileDialog(self, message='Open', wildcard=wildcard,
                           style=wx.OPEN) as dlg:  # 11
            if dlg.ShowModal() == wx.ID_OK:  # 12
                directory, filename = dlg.GetDirectory(), dlg.GetFilename()  # 13
                self.txt_ctrl.LoadFile('/'.join((directory, filename)))  # 14
                self.SetTitle('{} - Notespad'.format(filename))  # 15
if __name__ == '__main__':
    wx_app = wx.App(False)
    frame = Notespad(None)
    frame.Show()
    wx_app.MainLoop()

  1. Creates the Open menu item
  2. Adds a seperator to the menu
  3. Changed txt_ctrl to an instance variable so it can be accessed in methods
  4. Altered to account for the txt_ctrl name change
  5. Creates the Open event
  6. Creates the open event method
  7. Calls the file_open method
  8. Tell the app to continue processing events of this type
  9. method definition
  10. Creates a variable with the file types that can be opened
  11. Opens a FileDialog by using with will destroy the dialog when its finished with
  12. Shows the dialog and waits for a result back, if the result is a filename it will continue
  13. Grabs the directory and filename that was selected and stores them in variables
  14. Loads the text from the selected file into our txt_ctrl
  15. Alters the frame title to the name of the file that was opened
Reply
#3
Time to add the ability to save a file and to open a new file, as im making this up as i go there have been some lines deleted or modified as well as new lines added
import wx

class Notespad(wx.Frame):
    UNTITLED = 'Untitled'  #
    WILDCARD = 'Text Documents (*.txt)|*.txt|Python Documents (*.py)|*.py'  #
    def __init__(self, *args, **kwargs):
        #----------------------------------------------------------- Attributes
        self.file_directory = None  #
        self.file_name = self.UNTITLED  #
        self.title_string = '{}{} - NotesPad'  #
        #---------------------------------------------------------- Frame Setup
        super(Notespad, self).__init__(*args, **kwargs)
        self.CreateStatusBar()
        #----------------------------------------------------------- Frame Menu
        menubar = wx.MenuBar()
        self.SetMenuBar(menubar)
        file_menu = wx.Menu()
        menu_open = file_menu.Append(wx.ID_OPEN, '&Open',
                                     'Open an existing document')
        menu_new = file_menu.Append(wx.ID_NEW, '&New',
                                    'Creates a new document')  #
        menu_save = file_menu.Append(wx.ID_SAVE, '&Save',
                                     'Saves the active document')  #
        menu_saveas = file_menu.Append(wx.ID_SAVEAS, 'Save &As',
                                'Saves the active document with a new name')  #
        file_menu.AppendSeparator()
        menu_exit = file_menu.Append(-1, 'Exit', 'Exit the Application')
        menubar.Append(file_menu, '&File')
        #--------------------------------------------------- Panel And Controls
        panel = wx.Panel(self)
        self.txt_ctrl = wx.TextCtrl(panel, style=wx.TE_MULTILINE)
        #------------------------------------------------------- Sizer Creation
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(panel, 1, wx.EXPAND)
        p_sizer = wx.BoxSizer(wx.VERTICAL)
        p_sizer.Add(self.txt_ctrl, 1, wx.EXPAND)
        #------------------------------------------------------- Setting Sizers
        panel.SetSizer(p_sizer)
        self.SetSizer(sizer)
        self.Layout()
        #---------------------------------------------------------- Event Binds
        self.Bind(wx.EVT_MENU, self.on_menu_exit, menu_exit)
        self.Bind(wx.EVT_MENU, self.on_menu_open, menu_open)
        self.Bind(wx.EVT_MENU, self.on_menu_new, menu_new)  #
        self.Bind(wx.EVT_MENU, self.on_menu_save, menu_save)  #
        self.Bind(wx.EVT_MENU, self.on_menu_saveas, menu_saveas)  #
        #-------------------------------------------------------- Initial State
        self.set_title()  #
    #----------------------------------------------------------- Event Handlers
    def on_menu_exit(self, event):
        self.Close()
        event.Skip()
    def on_menu_open(self, event):
        self.file_open()
        event.Skip()
    def on_menu_new(self, event):  #
        self.file_new()  #
        event.Skip()  #
    def on_menu_save(self, event):  #
        self.file_save()  #
        event.Skip()  #
    def on_menu_saveas(self, event):  #
        self.file_saveas()  #
        event.Skip()  #
    #------------------------------------------------------------------ Actions
    def file_open(self):
        with wx.FileDialog(self, message='Open', wildcard=self.WILDCARD,
                           style=wx.FD_OPEN) as dlg:
            if dlg.ShowModal() == wx.ID_OK:
                self.file_directory = dlg.GetDirectory()  #
                self.file_name = dlg.GetFilename()  #
                self.file_load()  #
    def file_load(self):  #
        full_path = '/'.join((self.file_directory, self.file_name))  #
        self.txt_ctrl.LoadFile(full_path)  #
        self.set_title()  #
    def file_new(self):  #
        self.file_directory = None  #
        self.file_name = self.UNTITLED  #
        self.txt_ctrl.Clear()  #
        self.set_title()  #
    def file_saveas(self):  #
        with wx.FileDialog(self, message='Save as', wildcard=self.WILDCARD,
                           style=wx.FD_SAVE) as dlg:  #
            if dlg.ShowModal() == wx.ID_OK:  #
                self.file_directory = dlg.GetDirectory()  #
                self.file_name = dlg.GetFilename()  #
                self.file_save()  #
    def file_save(self):  #
        if self.file_name == self.UNTITLED:  #
            self.file_saveas()  #
        else:  #
            full_path = '/'.join((self.file_directory, self.file_name))  #
            self.txt_ctrl.SaveFile(full_path)  #
            self.set_title()  #
    def set_title(self):  #
        is_modified = '*' if self.txt_ctrl.IsModified() else ''  #
        self.SetTitle(self.title_string.format(is_modified, self.file_name))  #

if __name__ == '__main__':
    wx_app = wx.App(False)
    frame = Notespad(None)
    frame.Show()
    wx_app.MainLoop()

  1. The list of modifications will follow for the time being new/changed line end with #
Is any one following this and have any questions or comments so far  :?:
Reply
#4
Sizers were always my bane in wxPython. I've never been able to figure out their associated attributes.
Reply
#5
(Sep-27-2016, 05:50 AM)llanitedave Wrote: Sizers were always my bane in wxPython.  I've never been able to figure out their associated attributes.

Same here, even when I was still tinkering with wxWidgets in C++, everything went rather well except the part with sizers =))
Reply
#6
I've taken this back a step, updated to python 3 and refactored the code a bit.

The only change i needed to update to python 3 was
style=wx.OPEN
to
style=wx.FD_OPEN
I've also changed
self.SetTitle('{} - Notespad'.format(filename))
to use new string formating
self.SetTitle(f'{filename} - Notespad')
The new code
import wx


class Notespad(wx.Frame):
 
    def __init__(self, *args, **kwargs):
        super(Notespad, self).__init__(*args, **kwargs)
        self.frame_settings()
        self.create_menu()
        self.create_gui_items()
        self.create_sizers()
         
    def frame_settings(self):
        self.SetTitle('Untitled - Notespad')
        self.CreateStatusBar()
         
    def create_menu(self):
        menubar = wx.MenuBar()
        self.SetMenuBar(menubar)
        file_menu = wx.Menu()
        menu_open = file_menu.Append(wx.ID_OPEN, '&Open',
                                     'Open an existing file')
        file_menu.AppendSeparator()
        menu_exit = file_menu.Append(-1, 'E&xit', 'Exit the Application')
        menubar.Append(file_menu, '&File')
         
        self.Bind(wx.EVT_MENU, self.on_menu_exit, menu_exit)
        self.Bind(wx.EVT_MENU, self.on_menu_open, menu_open)
         
    def create_gui_items(self):
        self.frame_panel = wx.Panel(self)
        self.txt_ctrl = wx.TextCtrl(self.frame_panel,
                                    style=wx.TE_MULTILINE | wx.BORDER_NONE)
         
    def create_sizers(self):
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(self.frame_panel, 1, wx.EXPAND)
        p_sizer = wx.BoxSizer(wx.VERTICAL)
        p_sizer.Add(self.txt_ctrl, 1, wx.EXPAND)
        self.frame_panel.SetSizer(p_sizer)
        self.SetSizer(sizer)
        self.Layout()
 
    def on_menu_exit(self, event):
        self.Close()
        event.Skip()
 
    def on_menu_open(self, event):
        wildcard = 'Text Documents (*.txt)|*.txt|Python Documents (*.py)|*.py'
        with wx.FileDialog(self, message='Open', wildcard=wildcard,
                           style=wx.FD_OPEN) as dlg:
            if dlg.ShowModal() == wx.ID_OK:
                directory, filename = dlg.GetDirectory(), dlg.GetFilename()
                self.load_file_to_txt_ctrl(directory, filename)
                 
        event.Skip()
                 
    def load_file_to_txt_ctrl(self, directory, filename):
        self.txt_ctrl.LoadFile('/'.join((directory, filename)))
        self.SetTitle(f'{filename} - Notespad')
 
 
if __name__ == '__main__':
    wx_app = wx.App(False)
    frame = Notespad(None)
    frame.Show()
    wx_app.MainLoop()
Reply
#7
Adding the opening of a new file to the file menu.

In the method create_menu adding the following will create a new menu item.
docs.wxpython.org - wx.Menu
menu_new = file_menu.Append(wx.ID_NEW, '&New', 'Creates a new document')  # 1
Parameters
  • wx.ID_NEW The id that is given to the control, it is using one of the stock menu item id's
  • '&New' This is the string that is displayed in the menu, by placing & in front of a letter N makes it the menu shortcut key.
  • 'Creates a new document' This string will be displayed in the status bar when the menu item is hovered over.

The following binds a method to be called when the New menu item is clicked.
docs.wxpython.org - Events and Event Handling
self.Bind(wx.EVT_MENU, self.on_menu_new, menu_new)  # 2
Parameters
  • wx.EVT_MENU this is the event type, triggered by clicking on a menu item
  • self.on_menu_new The method that will be called when this event is triggered, Note: no () on the end so it only gets called when the event is triggered.
  • menu_new The menu item that triggers the event

Create the method on_menu_new
    def on_menu_new(self, event):  # 3
        self.txt_ctrl.Clear()  # 4
        self.SetTitle('Untitled - Notespad')  # 5
        event.Skip()  # 6

The full code now.
import wx


class Notespad(wx.Frame):
 
    def __init__(self, *args, **kwargs):
        super(Notespad, self).__init__(*args, **kwargs)
        self.frame_settings()
        self.create_menu()
        self.create_gui_items()
        self.create_sizers()
         
    def frame_settings(self):
        self.SetTitle('Untitled - Notespad')
        self.CreateStatusBar()
         
    def create_menu(self):
        menubar = wx.MenuBar()
        self.SetMenuBar(menubar)
        file_menu = wx.Menu()
        menu_open = file_menu.Append(wx.ID_OPEN, '&Open',
                                     'Open an existing file')
        
        menu_new = file_menu.Append(wx.ID_NEW, '&New',
                                    'Creates a new document')  # 1
        
        file_menu.AppendSeparator()
        menu_exit = file_menu.Append(-1, 'E&xit', 'Exit the Application')
        menubar.Append(file_menu, '&File')
         
        self.Bind(wx.EVT_MENU, self.on_menu_exit, menu_exit)
        self.Bind(wx.EVT_MENU, self.on_menu_open, menu_open)
        
        self.Bind(wx.EVT_MENU, self.on_menu_new, menu_new)  # 2
         
    def create_gui_items(self):
        self.frame_panel = wx.Panel(self)
        self.txt_ctrl = wx.TextCtrl(self.frame_panel,
                                    style=wx.TE_MULTILINE | wx.BORDER_NONE)
         
    def create_sizers(self):
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(self.frame_panel, 1, wx.EXPAND)
        p_sizer = wx.BoxSizer(wx.VERTICAL)
        p_sizer.Add(self.txt_ctrl, 1, wx.EXPAND)
        self.frame_panel.SetSizer(p_sizer)
        self.SetSizer(sizer)
        self.Layout()
 
    def on_menu_exit(self, event):
        self.Close()
        event.Skip()
 
    def on_menu_open(self, event):
        wildcard = 'Text Documents (*.txt)|*.txt|Python Documents (*.py)|*.py'
        with wx.FileDialog(self, message='Open', wildcard=wildcard,
                           style=wx.FD_OPEN) as dlg:
            if dlg.ShowModal() == wx.ID_OK:
                directory, filename = dlg.GetDirectory(), dlg.GetFilename()
                self.load_file_to_txt_ctrl(directory, filename)
                 
        event.Skip()
        
    def on_menu_new(self, event):  # 3
        self.txt_ctrl.Clear()  # 4
        self.SetTitle('Untitled - Notespad')  # 5
        event.Skip()  # 6
                 
    def load_file_to_txt_ctrl(self, directory, filename):
        self.txt_ctrl.LoadFile('/'.join((directory, filename)))
        self.SetTitle(f'{filename} - Notespad')
 
 
if __name__ == '__main__':
    wx_app = wx.App(False)
    frame = Notespad(None)
    frame.Show()
    wx_app.MainLoop()
Reply
#8
At the moment, if there is a file already open that has been modified but not saved and you open another file or a new file, the changes will be lost.
To keep track of the file that is open i'm going to use pathlib
from pathlib import Path
I added self.path = None to the Notespad __init__ to indicate that no file is open when the program starts.
and added a constant UNTITLED = 'Untitled' for an unsaved file name.

The following new method when called will set the frames title based on the paths name or set it to untitled
    def update_title(self):
        name = UNTITLED
        if self.path:
            name = self.path.name
        self.SetTitle(f'{name} - Notespad')
The following new method uses a wx.MessageDialog to ask if changes are to be saved
    def save_changes_msg_dialog(self):
        path = self.path or UNTITLED
        dlg = wx.MessageDialog(
            self, (f'Do you want to save changes to {path}?'),
            'Notespad', wx.YES_NO | wx.CANCEL | wx.CENTER)
        dlg.SetYesNoLabels('Save', 'Don\'t Save')
        return dlg.ShowModal()
The following new method uses self.txt_ctrl.IsModified to find out if the contents of the text control has been altered
    def save_if_modified(self):
        saved_or_dont_save = True
        if self.txt_ctrl.IsModified():
            dlg_result = self.save_changes_msg_dialog()
            if dlg_result == wx.ID_CANCEL:
                saved_or_dont_save = False
            elif dlg_result == wx.ID_YES:
                self.save()
                if not self.path:
                    saved_or_dont_save = False

        return saved_or_dont_save
If it has been altered it uses the self.save_changes_msg_dialog method to save the changes if required and to return true if its OK to precede or false if saving was cancelled or it was decided to cancel the action.

Here is the save and saveas methods
    def save(self):
        if not self.path: # 1
            self.save_as() #1
        else:  #
            self.txt_ctrl.SaveFile(str(self.path))
            self.update_title()

    def save_as(self):
        with wx.FileDialog(self, message='Save as', wildcard=WILDCARD,
                           style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT) as dlg:
            if dlg.ShowModal() == wx.ID_OK:
                directory, filename = dlg.GetDirectory(), dlg.GetFilename()
                self.path = Path(directory).joinpath(filename)
                self.save()
save calls save_as if there is no path, otherwise the contents of the text control is saved to the path and the title updated.
save_as opens up a wx.FileDialog that asks where to save the file to, if a file is chosen, self.path is set to the chosen file and save is called

on_menu_new and on_menu_open have been changed to call self.save_if_modified and only carry out opening a new file or opening an existing file if ok to do so.
    def on_menu_new(self, event):
        if self.save_if_modified():
            self.new()

    def on_menu_open(self, event):
        if self.save_if_modified():
            self.open()

    def on_menu_save(self, event):
        self.save()
        
    def on_menu_saveas(self, event):
        self.save_as()
The new method is quite straightforward
    def new(self):
        self.txt_ctrl.Clear()
        self.path = None
        self.update_title()
The open methid is making use of the pathlib Path
    def open(self):
        with wx.FileDialog(self, message='Open', wildcard=WILDCARD,
                           style=wx.FD_OPEN) as dlg:
            if dlg.ShowModal() == wx.ID_OK:
                directory, filename = dlg.GetDirectory(), dlg.GetFilename()
                self.path = Path(directory).joinpath(filename)  #
                self.txt_ctrl.LoadFile(str(self.path))  #
                self.update_title()
A new event bind has been added
self.Bind(wx.EVT_CLOSE, self.on_close_evt)
this is triggered by either the close method called from the menu close or from clicking the X on the frame.

The event itself
    def on_close_evt(self, event):
        if event.CanVeto():
            if not self.save_if_modified():
                event.Veto()
                return

        self.Destroy()
checks if it can interrupt the closing of the frame, it will then also check if unsaved changes should be saved. the window will only be destroyed if
there where no changes or if changes were made they were saved or decided to not save.

The menu items save and savas where added, the binds made and the events to call the save and saveas methods
        self.menu_save = file_menu.Append(wx.ID_SAVE, '&Save',
                                     'Saves the active document')
        self.menu_saveas = file_menu.Append(wx.ID_SAVEAS, 'Save &As',
                                'Saves the active document with a new name')
        self.Bind(wx.EVT_MENU, self.on_menu_save, self.menu_save)
        self.Bind(wx.EVT_MENU, self.on_menu_saveas, self.menu_saveas)
    def on_menu_save(self, event):
        self.save()
        
    def on_menu_saveas(self, event):
        self.save_as()
Here is the new full code
from pathlib import Path

import wx

WILDCARD = 'Text Documents (*.txt)|*.txt|Python Documents (*.py)|*.py'
UNTITLED = 'Untitled'


class Notespad(wx.Frame):

    def __init__(self, *args, **kwargs):
        super(Notespad, self).__init__(*args, **kwargs)
        self.path = None
        self.frame_settings()
        self.create_menu()
        self.create_gui_items()
        self.create_sizers()
        self.create_binds()

    def frame_settings(self):
        self.update_title()
        self.CreateStatusBar()
        self.SetMinSize((600, 400))
        self.CentreOnScreen()

    def create_menu(self):
        menubar = wx.MenuBar()
        self.SetMenuBar(menubar)
        file_menu = wx.Menu()
        self.menu_new = file_menu.Append(wx.ID_NEW, '&New',
                                    'Creates a new document')
        self.menu_open = file_menu.Append(wx.ID_OPEN, '&Open',
                                     'Open an existing file')
        self.menu_save = file_menu.Append(wx.ID_SAVE, '&Save',
                                     'Saves the active document')
        self.menu_saveas = file_menu.Append(wx.ID_SAVEAS, 'Save &As',
                                'Saves the active document with a new name')
        file_menu.AppendSeparator()
        self.menu_exit = file_menu.Append(-1, 'E&xit', 'Exit the Application')
        menubar.Append(file_menu, '&File')

    def create_gui_items(self):
        self.frame_panel = wx.Panel(self)
        self.txt_ctrl = wx.TextCtrl(self.frame_panel,
                                    style=wx.TE_MULTILINE | wx.BORDER_NONE)

    def create_sizers(self):
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(self.frame_panel, 1, wx.EXPAND)
        p_sizer = wx.BoxSizer(wx.VERTICAL)
        p_sizer.Add(self.txt_ctrl, 1, wx.EXPAND)
        self.frame_panel.SetSizer(p_sizer)
        self.SetSizer(sizer)
        self.Layout()

    def create_binds(self):
        self.Bind(wx.EVT_MENU, self.on_menu_new, self.menu_new)
        self.Bind(wx.EVT_MENU, self.on_menu_open, self.menu_open)
        self.Bind(wx.EVT_MENU, self.on_menu_save, self.menu_save)
        self.Bind(wx.EVT_MENU, self.on_menu_saveas, self.menu_saveas)
        self.Bind(wx.EVT_MENU, self.on_menu_exit, self.menu_exit)

        self.Bind(wx.EVT_CLOSE, self.on_close_evt)

    def on_menu_new(self, event):
        if self.save_if_modified():
            self.new()

    def on_menu_open(self, event):
        if self.save_if_modified():
            self.open()
            
    def on_menu_save(self, event):
        self.save()
        
    def on_menu_saveas(self, event):
        self.save_as()

    def on_menu_exit(self, event):
        self.Close()

    def on_close_evt(self, event):
        if event.CanVeto():
            if not self.save_if_modified():
                event.Veto()
                return

        self.Destroy()

    def update_title(self):
        name = UNTITLED
        if self.path:
            name = self.path.name
        self.SetTitle(f'{name} - Notespad')

    def save_changes_msg_dialog(self):
        path = self.path or UNTITLED
        dlg = wx.MessageDialog(
            self, (f'Do you want to save changes to {path}?'),
            'Notespad', wx.YES_NO | wx.CANCEL | wx.CENTER)
        dlg.SetYesNoLabels('Save', 'Don\'t Save')
        return dlg.ShowModal()

    def save_if_modified(self):
        saved_or_dont_save = True
        if self.txt_ctrl.IsModified():
            dlg_result = self.save_changes_msg_dialog()
            if dlg_result == wx.ID_CANCEL:
                saved_or_dont_save = False
            elif dlg_result == wx.ID_YES:
                self.save()
                if not self.path:
                    saved_or_dont_save = False

        return saved_or_dont_save

    def new(self):
        self.txt_ctrl.Clear()
        self.path = None
        self.update_title()

    def open(self):
        with wx.FileDialog(self, message='Open', wildcard=WILDCARD,
                           style=wx.FD_OPEN) as dlg:
            if dlg.ShowModal() == wx.ID_OK:
                directory, filename = dlg.GetDirectory(), dlg.GetFilename()
                self.path = Path(directory).joinpath(filename)
                self.txt_ctrl.LoadFile(str(self.path))
                self.update_title()

    def save(self):
        if not self.path:
            self.save_as()
        else:  #
            self.txt_ctrl.SaveFile(str(self.path))
            self.update_title()

    def save_as(self):
        with wx.FileDialog(self, message='Save as', wildcard=WILDCARD,
                           style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT) as dlg:
            if dlg.ShowModal() == wx.ID_OK:
                directory, filename = dlg.GetDirectory(), dlg.GetFilename()
                self.path = Path(directory).joinpath(filename)
                self.save()


if __name__ == '__main__':
    wx_app = wx.App(False)
    frame = Notespad(None)
    frame.Show()
    wx_app.MainLoop()
Reply
#9
Added a help menu that opens this forum thread to ask for help or any questions.
add an import
import webbrowser
A url constant is added
HELP_URL = 'https://python-forum.io/Thread-WxPython-Tutorial-Notespad-W-I-P'
A new menu is created and a help item is added to it
        help_menu = wx.Menu()
        self.menu_help = help_menu.Append(wx.ID_HELP, '&Help',
                                         'Get Help from python-forum.io')
        menubar.Append(help_menu, '&Help')
A simple bind
self.Bind(wx.EVT_MENU, self.on_menu_help, self.menu_help)
A simple event calls the open_forum_page method
    def on_menu_help(self, event):
        self.open_forum_page()
And this open a web browser to this forum thread
    def open_forum_page(self):
        webbrowser.open(HELP_URL, 2)
Full code
from pathlib import Path
import webbrowser

import wx

WILDCARD = 'Text Documents (*.txt)|*.txt|Python Documents (*.py)|*.py'
UNTITLED = 'Untitled'
HELP_URL = 'https://python-forum.io/Thread-WxPython-Tutorial-Notespad-W-I-P'


class Notespad(wx.Frame):

    def __init__(self, *args, **kwargs):
        super(Notespad, self).__init__(*args, **kwargs)
        self.path = None
        self.frame_settings()
        self.create_menu()
        self.create_gui_items()
        self.create_sizers()
        self.create_binds()

    def frame_settings(self):
        self.update_title()
        self.CreateStatusBar()
        self.SetMinSize((600, 400))
        self.CentreOnScreen()

    def create_menu(self):
        menubar = wx.MenuBar()
        self.SetMenuBar(menubar)
        file_menu = wx.Menu()
        self.menu_new = file_menu.Append(wx.ID_NEW, '&New',
                                    'Creates a new document')
        self.menu_open = file_menu.Append(wx.ID_OPEN, '&Open',
                                     'Open an existing file')
        self.menu_save = file_menu.Append(wx.ID_SAVE, '&Save',
                                     'Saves the active document')
        self.menu_saveas = file_menu.Append(wx.ID_SAVEAS, 'Save &As',
                                'Saves the active document with a new name')
        file_menu.AppendSeparator()
        self.menu_exit = file_menu.Append(-1, 'E&xit', 'Exit the Application')
        menubar.Append(file_menu, '&File')
        
        help_menu = wx.Menu()
        self.menu_help = help_menu.Append(wx.ID_HELP, '&Help',
                                         'Get Help from python-forum.io')
        menubar.Append(help_menu, '&Help')

    def create_gui_items(self):
        self.frame_panel = wx.Panel(self)
        self.txt_ctrl = wx.TextCtrl(self.frame_panel,
                                    style=wx.TE_MULTILINE | wx.BORDER_NONE)

    def create_sizers(self):
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(self.frame_panel, 1, wx.EXPAND)
        p_sizer = wx.BoxSizer(wx.VERTICAL)
        p_sizer.Add(self.txt_ctrl, 1, wx.EXPAND)
        self.frame_panel.SetSizer(p_sizer)
        self.SetSizer(sizer)
        self.Layout()

    def create_binds(self):
        self.Bind(wx.EVT_MENU, self.on_menu_new, self.menu_new)
        self.Bind(wx.EVT_MENU, self.on_menu_open, self.menu_open)
        self.Bind(wx.EVT_MENU, self.on_menu_save, self.menu_save)
        self.Bind(wx.EVT_MENU, self.on_menu_saveas, self.menu_saveas)
        self.Bind(wx.EVT_MENU, self.on_menu_help, self.menu_help)
        self.Bind(wx.EVT_MENU, self.on_menu_exit, self.menu_exit)

        self.Bind(wx.EVT_CLOSE, self.on_close_evt)

    def on_menu_new(self, event):
        if self.save_if_modified():
            self.new()

    def on_menu_open(self, event):
        if self.save_if_modified():
            self.open()
            
    def on_menu_save(self, event):
        self.save()
        
    def on_menu_saveas(self, event):
        self.save_as()
        
    def on_menu_help(self, event):
        self.open_forum_page()

    def on_menu_exit(self, event):
        self.Close()

    def on_close_evt(self, event):
        if event.CanVeto():
            if not self.save_if_modified():
                event.Veto()
                return

        self.Destroy()

    def update_title(self):
        name = UNTITLED
        if self.path:
            name = self.path.name
        self.SetTitle(f'{name} - Notespad')

    def save_changes_msg_dialog(self):
        path = self.path or UNTITLED
        dlg = wx.MessageDialog(
            self, (f'Do you want to save changes to {path}?'),
            'Notespad', wx.YES_NO | wx.CANCEL | wx.CENTER)
        dlg.SetYesNoLabels('Save', 'Don\'t Save')
        return dlg.ShowModal()

    def save_if_modified(self):
        saved_or_dont_save = True
        if self.txt_ctrl.IsModified():
            dlg_result = self.save_changes_msg_dialog()
            if dlg_result == wx.ID_CANCEL:
                saved_or_dont_save = False
            elif dlg_result == wx.ID_YES:
                self.save()
                if not self.path:
                    saved_or_dont_save = False

        return saved_or_dont_save

    def new(self):
        self.txt_ctrl.Clear()
        self.path = None
        self.update_title()

    def open(self):
        with wx.FileDialog(self, message='Open', wildcard=WILDCARD,
                           style=wx.FD_OPEN) as dlg:
            if dlg.ShowModal() == wx.ID_OK:
                directory, filename = dlg.GetDirectory(), dlg.GetFilename()
                self.path = Path(directory).joinpath(filename)
                self.txt_ctrl.LoadFile(str(self.path))
                self.update_title()

    def save(self):
        if not self.path:
            self.save_as()
        else:  #
            self.txt_ctrl.SaveFile(str(self.path))
            self.update_title()

    def save_as(self):
        with wx.FileDialog(self, message='Save as', wildcard=WILDCARD,
                           style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT) as dlg:
            if dlg.ShowModal() == wx.ID_OK:
                directory, filename = dlg.GetDirectory(), dlg.GetFilename()
                self.path = Path(directory).joinpath(filename)
                self.save()

    def open_forum_page(self):
        webbrowser.open(HELP_URL, 2)


if __name__ == '__main__':
    wx_app = wx.App(False)
    frame = Notespad(None)
    frame.Show()
    wx_app.MainLoop()
Reply
#10
Similar to the other menu items, adding a edit menu with undo, cut, copy, paste and delete
        edit_menu = wx.Menu()
        self.menu_undo = edit_menu.Append(wx.ID_UNDO, '&Undo',
                                    'Undo change')
        edit_menu.AppendSeparator()
        self.menu_cut = edit_menu.Append(wx.ID_CUT, 'Cu&t',
                                    'Cut the selected text')
        self.menu_copy = edit_menu.Append(wx.ID_COPY, '&Copy',
                                    'Copy the selected text')
        self.menu_paste = edit_menu.Append(wx.ID_PASTE, '&Paste',
                                    'Paste from the clipboard')
        self.menu_delete = edit_menu.Append(wx.ID_DELETE, 'De&lete',
                                    'Delete selected text')
        self.menubar.Append(edit_menu, '&Edit')
binds for the new menu items
        self.Bind(wx.EVT_MENU, self.on_menu_undo, self.menu_undo)
        self.Bind(wx.EVT_UPDATE_UI, self.on_menu_undo_update, self.menu_undo)
        self.Bind(wx.EVT_MENU, self.on_menu_cut, self.menu_cut)
        self.Bind(wx.EVT_UPDATE_UI, self.on_menu_cut_update, self.menu_cut)
        self.Bind(wx.EVT_MENU, self.on_menu_copy, self.menu_copy)
        self.Bind(wx.EVT_UPDATE_UI, self.on_menu_copy_update, self.menu_copy)
        self.Bind(wx.EVT_MENU, self.on_menu_paste, self.menu_paste)
        self.Bind(wx.EVT_UPDATE_UI, self.on_menu_paste_update, self.menu_paste)
        self.Bind(wx.EVT_MENU, self.on_menu_delete, self.menu_delete)
        self.Bind(
            wx.EVT_UPDATE_UI, self.on_menu_delete_update, self.menu_delete)
These have a type of event that has not been used so far wx.EVT_UPDATE_UI
https://wxpython.org/Phoenix/docs/html/w...Event.html Wrote:This class is used for pseudo-events which are called by wxWidgets to give an application the chance to update various user interface elements. .... see link for more details

Here are the event handlers for undo
    def on_menu_undo(self, event):
        self.txt_ctrl.Undo()

    def on_menu_undo_update(self, event):
        event.Enable(self.txt_ctrl.CanUndo())
on_menu_undo is straight forward, on_menu_undo_update will enable or disable the menu item depending on if the text control can be undone or not.

The events for cut, copy and paste are similar to undo

For the delete events
    def on_menu_delete(self, event):
        self.txt_ctrl.Remove(*self.txt_ctrl.GetSelection())

    def on_menu_delete_update(self, event):
        event.Enable(bool(self.txt_ctrl.GetStringSelection()))
on_menu_delete uses the text controls remove method which removes the text starting at the first given position up to (but not including) the character at the last position.
The values for this are obtained by using text controls GetSelection method.

on_menu_delete_update enables or disables the delete menu item by using bool to get a true or false from the result of method GetSelection which gets the text currently selected in the control or if there is no selection, the returned string is empty.

Full Code
from pathlib import Path
import webbrowser

import wx

WILDCARD = 'Text Documents (*.txt)|*.txt|Python Documents (*.py)|*.py'
UNTITLED = 'Untitled'
HELP_URL = 'https://python-forum.io/Thread-WxPython-Tutorial-Notespad-W-I-P'


class Notespad(wx.Frame):

    def __init__(self, *args, **kwargs):
        super(Notespad, self).__init__(*args, **kwargs)
        self.path = None
        self.frame_settings()
        self.create_menus()
        self.create_gui_items()
        self.create_sizers()
        self.add_binds()

    def frame_settings(self):
        self.update_title()
        self.CreateStatusBar()
        self.SetMinSize((600, 400))
        self.CentreOnScreen()

    def create_menus(self):
        self.menubar = wx.MenuBar()
        self.SetMenuBar(self.menubar)
        self.create_file_menu()
        self.create_edit_menu()
        self.create_help_menu()

    def create_file_menu(self):
        file_menu = wx.Menu()
        self.menu_new = file_menu.Append(wx.ID_NEW, '&New',
                                    'Creates a new document')
        self.menu_open = file_menu.Append(wx.ID_OPEN, '&Open',
                                     'Open an existing file')
        self.menu_save = file_menu.Append(wx.ID_SAVE, '&Save',
                                     'Saves the active document')
        self.menu_saveas = file_menu.Append(wx.ID_SAVEAS, 'Save &As',
                                'Saves the active document with a new name')
        file_menu.AppendSeparator()
        self.menu_exit = file_menu.Append(-1, 'E&xit', 'Exit the Application')
        self.menubar.Append(file_menu, '&File')

    def create_edit_menu(self):
        edit_menu = wx.Menu()
        self.menu_undo = edit_menu.Append(wx.ID_UNDO, '&Undo',
                                    'Undo change')
        edit_menu.AppendSeparator()
        self.menu_cut = edit_menu.Append(wx.ID_CUT, 'Cu&t',
                                    'Cut the selected text')
        self.menu_copy = edit_menu.Append(wx.ID_COPY, '&Copy',
                                    'Copy the selected text')
        self.menu_paste = edit_menu.Append(wx.ID_PASTE, '&Paste',
                                    'Paste from the clipboard')
        self.menu_delete = edit_menu.Append(wx.ID_DELETE, 'De&lete',
                                    'Delete selected text')
        self.menubar.Append(edit_menu, '&Edit')

    def create_help_menu(self):
        help_menu = wx.Menu()
        self.menu_help = help_menu.Append(wx.ID_HELP, '&Help',
                                         'Get Help from python-forum.io')
        self.menubar.Append(help_menu, '&Help')

    def create_gui_items(self):
        self.frame_panel = wx.Panel(self)
        self.txt_ctrl = wx.TextCtrl(self.frame_panel,
                                    style=wx.TE_MULTILINE | wx.BORDER_NONE)

    def create_sizers(self):
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(self.frame_panel, 1, wx.EXPAND)
        p_sizer = wx.BoxSizer(wx.VERTICAL)
        p_sizer.Add(self.txt_ctrl, 1, wx.EXPAND)
        self.frame_panel.SetSizer(p_sizer)
        self.SetSizer(sizer)
        self.Layout()

    def add_binds(self):
        self.add_frame_binds()
        self.add_fle_menu_binds()
        self.add_edit_menu_binds()
        self.add_help_menu_binds()

    def add_frame_binds(self):
        self.Bind(wx.EVT_CLOSE, self.on_close_evt)

    def add_fle_menu_binds(self):
        self.Bind(wx.EVT_MENU, self.on_menu_new, self.menu_new)
        self.Bind(wx.EVT_MENU, self.on_menu_open, self.menu_open)
        self.Bind(wx.EVT_MENU, self.on_menu_save, self.menu_save)
        self.Bind(wx.EVT_MENU, self.on_menu_saveas, self.menu_saveas)
        self.Bind(wx.EVT_MENU, self.on_menu_exit, self.menu_exit)

    def add_edit_menu_binds(self):
        self.Bind(wx.EVT_MENU, self.on_menu_undo, self.menu_undo)
        self.Bind(wx.EVT_UPDATE_UI, self.on_menu_undo_update, self.menu_undo)
        self.Bind(wx.EVT_MENU, self.on_menu_cut, self.menu_cut)
        self.Bind(wx.EVT_UPDATE_UI, self.on_menu_cut_update, self.menu_cut)
        self.Bind(wx.EVT_MENU, self.on_menu_copy, self.menu_copy)
        self.Bind(wx.EVT_UPDATE_UI, self.on_menu_copy_update, self.menu_copy)
        self.Bind(wx.EVT_MENU, self.on_menu_paste, self.menu_paste)
        self.Bind(wx.EVT_UPDATE_UI, self.on_menu_paste_update, self.menu_paste)
        self.Bind(wx.EVT_MENU, self.on_menu_delete, self.menu_delete)
        self.Bind(
            wx.EVT_UPDATE_UI, self.on_menu_delete_update, self.menu_delete)

    def add_help_menu_binds(self):
        self.Bind(wx.EVT_MENU, self.on_menu_help, self.menu_help)

    def on_menu_exit(self, event):
        self.Close()

    def on_menu_new(self, event):
        if self.save_if_modified():
            self.new()

    def on_menu_open(self, event):
        if self.save_if_modified():
            self.open()

    def on_menu_save(self, event):
        self.save()

    def on_menu_saveas(self, event):
        self.save_as()

    def on_menu_undo(self, event):
        self.txt_ctrl.Undo()

    def on_menu_undo_update(self, event):
        event.Enable(self.txt_ctrl.CanUndo())

    def on_menu_cut(self, event):
        self.txt_ctrl.Cut()

    def on_menu_cut_update(self, event):
        event.Enable(self.txt_ctrl.CanCut())

    def on_menu_copy(self, event):
        self.txt_ctrl.Copy()

    def on_menu_copy_update(self, event):
        event.Enable(self.txt_ctrl.CanCopy())

    def on_menu_paste(self, event):
        self.txt_ctrl.Paste()

    def on_menu_paste_update(self, event):
        event.Enable(self.txt_ctrl.CanPaste())

    def on_menu_delete(self, event):
        self.txt_ctrl.Remove(*self.txt_ctrl.GetSelection())

    def on_menu_delete_update(self, event):
        event.Enable(bool(self.txt_ctrl.GetStringSelection()))

    def on_menu_help(self, event):
        self.open_forum_page()

    def on_close_evt(self, event):
        if event.CanVeto():
            if not self.save_if_modified():
                event.Veto()
                return

        self.Destroy()

    def update_title(self):
        name = UNTITLED
        if self.path:
            name = self.path.name
        self.SetTitle(f'{name} - Notespad')

    def save_changes_msg_dialog(self):
        path = self.path or UNTITLED
        dlg = wx.MessageDialog(
            self, (f'Do you want to save changes to {path}?'),
            'Notespad', wx.YES_NO | wx.CANCEL | wx.CENTER)
        dlg.SetYesNoLabels('Save', 'Don\'t Save')
        return dlg.ShowModal()

    def save_if_modified(self):
        saved_or_dont_save = True
        if self.txt_ctrl.IsModified():
            dlg_result = self.save_changes_msg_dialog()
            if dlg_result == wx.ID_CANCEL:
                saved_or_dont_save = False
            elif dlg_result == wx.ID_YES:
                self.save()
                if not self.path:
                    saved_or_dont_save = False

        return saved_or_dont_save

    def new(self):
        self.txt_ctrl.Clear()
        self.path = None
        self.update_title()

    def open(self):
        with wx.FileDialog(self, message='Open', wildcard=WILDCARD,
                           style=wx.FD_OPEN) as dlg:
            if dlg.ShowModal() == wx.ID_OK:
                directory, filename = dlg.GetDirectory(), dlg.GetFilename()
                self.path = Path(directory).joinpath(filename)
                self.txt_ctrl.LoadFile(str(self.path))
                self.update_title()

    def save(self):
        if not self.path:
            self.save_as()
        else:  #
            self.txt_ctrl.SaveFile(str(self.path))
            self.update_title()

    def save_as(self):
        with wx.FileDialog(self, message='Save as', wildcard=WILDCARD,
                           style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT) as dlg:
            if dlg.ShowModal() == wx.ID_OK:
                directory, filename = dlg.GetDirectory(), dlg.GetFilename()
                self.path = Path(directory).joinpath(filename)
                self.save()

    def open_forum_page(self):
        webbrowser.open(HELP_URL, 2)


if __name__ == '__main__':
    wx_app = wx.App(False)
    frame = Notespad(None)
    frame.Show()
    wx_app.MainLoop()
Reply


Forum Jump:

User Panel Messages

Announcements
Announcement #1 8/1/2020
Announcement #2 8/2/2020
Announcement #3 8/6/2020