Quantcast
Channel: Mike - Mouse Vs Python
Viewing all 12 articles
Browse latest View live

The “Book” Controls of wxPython (Part 2 of 2)

$
0
0

In the first part of this series, I wrote on all the non-agw notebook widgets included with wxPython. For this second article, I will be focusing on the two notebooks that are in the AGW library of wxPython. AGW stands for Advanced Generic Widgets, a set of widgets that are written in Python instead of wrapped C++ code. I personally think that AGW is also a callback to its amazing author, Andrea Gavana. Regardless, the two widgets in this review will be the FlatNotebook and another AUI Notebook. The FlatNotebook has a great demo and I will spend most of this article on demos I’ve created that are based on it. The AUI Notebook is a part of agw.aui. While the demo for agw.aui is cool, it focuses on AUI in general, not the notebook. So I’ll just show you what I can glean from that. Now, let’s get cracking!

Update: The API changed slightly when it comes to AGW-related widgets. Basically some style flags in wxPython 2.8.11.0+ now require the agw-specific style flags. To use them, you’ll need to use the agwStyle keyword. See Andrea’s docs for more info: http://xoomer.virgilio.it/infinity77/AGW_Docs/ If you run into an error, try changing that first or post to the mailing list.

The Amazing FlatNotebook

The Flatbook control is written in Python rather than a wrapped widget from wxWidgets. It was added to wxPython with the release of wxPython 2.8.9.2 on February 16, 2009. Since then Andrea Gavana has been updating the agw library with lots of fixes. My examples will work with the 2.8.9.2+ versions of wxPython, but I recommend getting the SVN version of agw and replacing your default one with it as there have been a lot of bug fixes applied to the AUI module and several others. There is also an effort going on currently to better document this library in the code itself, so you may find that helpful too!

Here are a few of the FlatNotebook’s Features:

  • 5 Different tab styles
  • It’s a generic control (i.e. pure python) so it’s easy to modify
  • You can use the mouse’s middle-click to close tabs
  • A built-in function to add right-click pop-up menus on tabs
  • A way to hide the “X” that closes the individual tabs
  • Support for disabled tabs
  • Plus lots more! See the source and the wxPython Demo for more information!

Now that we’ve done an unpaid commercial, let’s take a look at the actual product:

flatnotebookDemo

Listing 1

import panelOne, panelTwo, panelThree
import wx
import wx.lib.agw.flatnotebook as fnb
 
########################################################################
class FlatNotebookDemo(fnb.FlatNotebook):
    """
    Flatnotebook class
    """
 
    #----------------------------------------------------------------------
    def __init__(self, parent):
        """Constructor"""
        fnb.FlatNotebook.__init__(self, parent, wx.ID_ANY)
 
        pageOne = panelOne.TabPanel(self)
        pageTwo = panelTwo.TabPanel(self)
        pageThree = panelThree.TabPanel(self)
 
        self.AddPage(pageOne, "PageOne")
        self.AddPage(pageTwo, "PageTwo")
        self.AddPage(pageThree, "PageThree")  
 
########################################################################
class DemoFrame(wx.Frame):
    """
    Frame that holds all other widgets
    """
 
    #----------------------------------------------------------------------
    def __init__(self):
        """Constructor"""        
        wx.Frame.__init__(self, None, wx.ID_ANY, 
                          "FlatNotebook Tutorial",
                          size=(600,400)
                          )
        panel = wx.Panel(self)
 
        notebook = FlatNotebookDemo(panel)
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(notebook, 1, wx.ALL|wx.EXPAND, 5)
        panel.SetSizer(sizer)
        self.Layout()
 
        self.Show()
 
#----------------------------------------------------------------------
if __name__ == "__main__":
    app = wx.PySimpleApp()
    frame = DemoFrame()
    app.MainLoop()

In Listing 1, I subclass FlatNotebook and use the generic panels from my previous article for the pages. You’ll notice that FlatNotebook has its own AddPage method that mimics the wx.Notebook. This should come as no surprise as the FlatNotebook’s API is such that you should be able to use it as a drop-in replacement for wx.Notebook. Of course, right out of the box, FlatNotebook has the advantage. If you run the demo above, you’ll see that FlatNotebook allows the user to rearrange the tabs, close the tabs and it includes some previous/next buttons in case you have more tabs than can fit on-screen at once.

Now let’s take a look at the various styles that we can apply to FlatNotebook:

flatnotebookStyleDemo

Listing 2

import panelOne, panelTwo, panelThree
import wx
import wx.lib.agw.flatnotebook as fnb
 
########################################################################
class FlatNotebookDemo(fnb.FlatNotebook):
    """
    Flatnotebook class
    """
 
    #----------------------------------------------------------------------
    def __init__(self, parent):
        """Constructor"""
        fnb.FlatNotebook.__init__(self, parent, wx.ID_ANY)
 
        pageOne = panelOne.TabPanel(self)
        pageTwo = panelTwo.TabPanel(self)
        pageThree = panelThree.TabPanel(self)
 
        self.AddPage(pageOne, "PageOne")
        self.AddPage(pageTwo, "PageTwo")
        self.AddPage(pageThree, "PageThree")  
 
 
########################################################################
class DemoFrame(wx.Frame):
    """
    Frame that holds all other widgets
    """
 
    #----------------------------------------------------------------------
    def __init__(self):
        """Constructor"""        
        wx.Frame.__init__(self, None, wx.ID_ANY, 
                          "FlatNotebook Tutorial with Style",
                          size=(600,400)
                          )
        self.styleDict = {"Default":self.OnDefaultStyle,
                          "VC71":self.OnVC71Style,
                          "VC8":self.OnVC8Style,
                          "Fancy":self.OnFancyStyle,
                          "Firefox 2":self.OnFF2Style}
        choices = self.styleDict.keys()
 
        panel = wx.Panel(self)        
        self.notebook = FlatNotebookDemo(panel)
        self.styleCbo = wx.ComboBox(panel, wx.ID_ANY, "Default",
                                    wx.DefaultPosition, wx.DefaultSize,
                                    choices=choices, style=wx.CB_DROPDOWN)
        styleBtn = wx.Button(panel, wx.ID_ANY, "Change Style")
        styleBtn.Bind(wx.EVT_BUTTON, self.onStyle)
 
        # create some sizers
        sizer = wx.BoxSizer(wx.VERTICAL)
        hSizer = wx.BoxSizer(wx.HORIZONTAL)
 
        # add the widgets to the sizers
        sizer.Add(self.notebook, 1, wx.ALL|wx.EXPAND, 5)
        hSizer.Add(self.styleCbo, 0, wx.ALL|wx.CENTER, 5)
        hSizer.Add(styleBtn, 0, wx.ALL, 5)
        sizer.Add(wx.StaticLine(panel), 0, wx.ALL|wx.EXPAND, 5)
        sizer.Add(hSizer, 0, wx.ALL, 5)
 
        panel.SetSizer(sizer)
        self.Layout()
 
        self.Show()
 
    #----------------------------------------------------------------------
    def onStyle(self, event):
        """
        Changes the style of the tabs
        """
        print "in onStyle"
        style = self.styleCbo.GetValue()
        print style
        self.styleDict[style]()           
 
    # The following methods were taken from the wxPython 
    # demo for the FlatNotebook
    def OnFF2Style(self):
 
        style = self.notebook.GetWindowStyleFlag()
 
        # remove old tabs style
        mirror = ~(fnb.FNB_VC71 | fnb.FNB_VC8 | fnb.FNB_FANCY_TABS | fnb.FNB_FF2)
        style &= mirror
 
        style |= fnb.FNB_FF2
 
        self.notebook.SetWindowStyleFlag(style)
 
 
    def OnVC71Style(self):
 
        style = self.notebook.GetWindowStyleFlag()
 
        # remove old tabs style
        mirror = ~(fnb.FNB_VC71 | fnb.FNB_VC8 | fnb.FNB_FANCY_TABS | fnb.FNB_FF2)
        style &= mirror
 
        style |= fnb.FNB_VC71
 
        self.notebook.SetWindowStyleFlag(style)
 
 
    def OnVC8Style(self):
 
        style = self.notebook.GetWindowStyleFlag()
 
        # remove old tabs style
        mirror = ~(fnb.FNB_VC71 | fnb.FNB_VC8 | fnb.FNB_FANCY_TABS | fnb.FNB_FF2)
        style &= mirror
 
        # set new style
        style |= fnb.FNB_VC8
 
        self.notebook.SetWindowStyleFlag(style)
 
 
    def OnDefaultStyle(self):
 
        style = self.notebook.GetWindowStyleFlag()
 
        # remove old tabs style
        mirror = ~(fnb.FNB_VC71 | fnb.FNB_VC8 | fnb.FNB_FANCY_TABS | fnb.FNB_FF2)
        style &= mirror
 
        self.notebook.SetWindowStyleFlag(style)
 
 
    def OnFancyStyle(self):
 
        style = self.notebook.GetWindowStyleFlag()
 
        # remove old tabs style
        mirror = ~(fnb.FNB_VC71 | fnb.FNB_VC8 | fnb.FNB_FANCY_TABS | fnb.FNB_FF2)
        style &= mirror
 
        style |= fnb.FNB_FANCY_TABS
        self.notebook.SetWindowStyleFlag(style)
 
#----------------------------------------------------------------------
if __name__ == "__main__":
    app = wx.PySimpleApp()
    frame = DemoFrame()
    app.MainLoop()

That’s a lot of code for a “simple” example, but I think it will help us understand how to apply tab styles to our widget. I borrowed most of the methods from the wxPython demo, in case you didn’t notice. The primary talking point in this code is the contents of those methods, which are mostly the same. Here’s the main snippet to take away from this section:

style = self.notebook.GetWindowStyleFlag()
 
# remove old tabs style
mirror = ~(fnb.FNB_VC71 | fnb.FNB_VC8 | fnb.FNB_FANCY_TABS | fnb.FNB_FF2)
style &= mirror
style |= fnb.FNB_FF2
self.notebook.SetWindowStyleFlag(style)

First, we need to get the current style of the FlatNotebook. Then we use some fancy magic in the “mirror” line that creates a set of styles that we want to remove. The line, “style &= mirror” actually does the removing and then we add the style we wanted with “style |= fnb.FNB_FF2”. Finally, we use SetWindowStyleFlag() to actually apply the style to the widget. You may be wondering what’s up with all those goofy symbols (i.e. |, ~, &). Well, those are known as bitwise operators. I don’t use them much myself, so I recommend reading the Python documentation for full details as I don’t fully understand them myself.

For my next demo, I created a way to add and delete pages from the FlatNotebook. Let’s see how:

flatnotebookPageDemo

Listing 3

import panelOne, panelTwo, panelThree
import random
import wx
import wx.lib.agw.flatnotebook as fnb
 
########################################################################
class FlatNotebookDemo(fnb.FlatNotebook):
    """
    Flatnotebook class
    """
    #----------------------------------------------------------------------
    def __init__(self, parent):
        """Constructor"""
        fnb.FlatNotebook.__init__(self, parent, wx.ID_ANY)
 
########################################################################
class DemoFrame(wx.Frame):
    """
    Frame that holds all other widgets
    """
 
    #----------------------------------------------------------------------
    def __init__(self, title="FlatNotebook Add/Remove Page Tutorial"):
        """Constructor"""        
        wx.Frame.__init__(self, None, wx.ID_ANY, 
                          title=title,
                          size=(600,400)
                          )
        self._newPageCounter = 0
        panel = wx.Panel(self)
        self.createRightClickMenu()
 
        # create some widgets
        self.notebook = FlatNotebookDemo(panel)
        addPageBtn = wx.Button(panel, label="Add Page")
        addPageBtn.Bind(wx.EVT_BUTTON, self.onAddPage)
        removePageBtn = wx.Button(panel, label="Remove Page")
        removePageBtn.Bind(wx.EVT_BUTTON, self.onDeletePage)
        self.notebook.SetRightClickMenu(self._rmenu)
 
        # create some sizers
        sizer = wx.BoxSizer(wx.VERTICAL)
        btnSizer = wx.BoxSizer(wx.HORIZONTAL)
 
        # layout the widgets
        sizer.Add(self.notebook, 1, wx.ALL|wx.EXPAND, 5)
        btnSizer.Add(addPageBtn, 0, wx.ALL, 5)
        btnSizer.Add(removePageBtn, 0, wx.ALL, 5)
        sizer.Add(btnSizer)
        panel.SetSizer(sizer)
        self.Layout()
 
        self.Show()
 
    #----------------------------------------------------------------------
    def createRightClickMenu(self):
        """
        Based on method from flatnotebook demo
        """
        self._rmenu = wx.Menu()
        item = wx.MenuItem(self._rmenu, wx.ID_ANY, 
                           "Close Tab\tCtrl+F4", 
                           "Close Tab")
        self.Bind(wx.EVT_MENU, self.onDeletePage, item)
        self._rmenu.AppendItem(item)
 
    #----------------------------------------------------------------------
    def onAddPage(self, event):
        """
        This method is based on the flatnotebook demo
 
        It adds a new page to the notebook
        """
        caption = "New Page Added #" + str(self._newPageCounter)
        self.Freeze()
 
        self.notebook.AddPage(self.createPage(caption), caption, True)
        self.Thaw()
        self._newPageCounter = self._newPageCounter + 1
 
    #----------------------------------------------------------------------
    def createPage(self, caption):
        """
        Creates a notebook page from one of three
        panels at random and returns the new page
        """
        panel_list = [panelOne, panelTwo, panelThree]
        obj = random.choice(panel_list)
        page = obj.TabPanel(self.notebook)
        return page
 
    #----------------------------------------------------------------------
    def onDeletePage(self, event):
        """
        This method is based on the flatnotebook demo
 
        It removes a page from the notebook
        """
        self.notebook.DeletePage(self.notebook.GetSelection())
 
#----------------------------------------------------------------------
if __name__ == "__main__":
    app = wx.PySimpleApp()
    frame = DemoFrame()
    app.MainLoop()

The code above allows the user to add as many pages as they want by clicking the Add Page button. The Remove Page button will remove whatever page is currently selected. When adding a page, but button handler freezes the frame and calls the notebook’s AddPage method. This calls the “createPage” method which randomly grabs one of my pre-defined panels, instantiates it and returns it to the AddPage method. On returning to the “onAddPage” method, the frame is thawed and the page counter is incremented.

The Remove Page button calls the notebook’s GetSelection() method to get the currently selected tab and then calls the notebook’s DeletePage() method to remove it from the notebook.

Another fun functionality that I enabled was the tab right-click menu, which gives us another way to close a tab, although you could use to do other actions as well. All you need to do to enable it is to call the notebook’s SetRightClickMenu() method and pass in a wx.Menu object.

There are tons of other features for you to explore as well. Be sure to check out the FlatNotebook demo in the official wxPython demo where you can learn to close tabs with the middle mouse button or via double-clicks, turn on gradient colors for the tab background, disable tabs, enable smart tabbing (which is kind of like the alt+tab menu in Windows), create drag-and-drop tabs between notebooks and much, much more!

AGW AUI Notebook

agwAuiNotebookDemo

Andrea Gavana went to the trouble of creating a pure python version of the Advanced User Interface (AUI) that provides perspective saving, floating sub-windows that can be docked, customizable look and feel and the splittable AUI Notebook. His notebook will be the focus of this section. The AGW AUI Notebook has lots of features, but I’m just going to go over some of the basics. If you want to see all the features, be sure to read the code and check out the demo in the official wxPython Demo. As I mentioned at the beginning of this tutorial, be sure to download the latest version of AUI (or AGW as a whole) from SVN to get all the bug fixes.

Let’s take a look at the simple example I used for the screenshot above:

Listing 4

#----------------------------------------------------------------------
# agwAUINotebook.py
#
# Created: December 2009
#
# Author: Mike Driscoll - mike@pythonlibrary.org
#
# Note: Some code comes from the wxPython demo
#
#----------------------------------------------------------------------
 
 
import wx
import wx.lib.agw.aui as aui 
 
########################################################################
class TabPanelOne(wx.Panel):
    """
    A simple wx.Panel class
    """
    #----------------------------------------------------------------------
    def __init__(self, parent):
        """"""
        wx.Panel.__init__(self, parent=parent, id=wx.ID_ANY)
 
        sizer = wx.BoxSizer(wx.VERTICAL)
        txtOne = wx.TextCtrl(self, wx.ID_ANY, "")
        txtTwo = wx.TextCtrl(self, wx.ID_ANY, "")
 
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(txtOne, 0, wx.ALL, 5)
        sizer.Add(txtTwo, 0, wx.ALL, 5)
 
        self.SetSizer(sizer)
 
########################################################################
class DemoFrame(wx.Frame):
    """
    wx.Frame class
    """
 
    #----------------------------------------------------------------------
    def __init__(self):
        wx.Frame.__init__(self, None, wx.ID_ANY, 
                          "AGW AUI Notebook Tutorial",
                          size=(600,400))
 
        self._mgr = aui.AuiManager()
 
        # tell AuiManager to manage this frame
        self._mgr.SetManagedWindow(self)
 
        notebook = aui.AuiNotebook(self)
        panelOne = TabPanelOne(notebook)
        panelTwo = TabPanelOne(notebook)
 
        notebook.AddPage(panelOne, "PanelOne", False)
        notebook.AddPage(panelTwo, "PanelTwo", False)
 
        self._mgr.AddPane(notebook, 
                          aui.AuiPaneInfo().Name("notebook_content").
                          CenterPane().PaneBorder(False)) 
        self._mgr.Update()
        #notebook.EnableTab(1, False)
 
 #----------------------------------------------------------------------
# Run the program
if __name__ == "__main__":
    app = wx.PySimpleApp()
    frame = DemoFrame()
    frame.Show()
    app.MainLoop()

The first difference between this notebook and the original AuiNotebook is that this one requires an AuiManager object. It may be that something similar is behind the original as well, but that’s hidden from us. Anyway, the first step is instantiating the AuiManager and then giving it the frame to manage via its SetManagedWindow() method. Now we can add the AUI Notebook. Note that we pass the frame as the parent of the notebook instead of the AuiManager. I think the reason is that when the AuiManager is given the frame, it becomes the top level window.

The next part of the equation should look familiar: AddPage(). Let’s see what it accepts:

AddPage(self, page, caption, select=False, bitmap=wx.NullBitmap, disabled_bitmap=wx.NullBitmap, control=None)

In my code, I only pass in the first three parameters, but you can also add a couple bitmaps and a wx.Window for the control. The next bit is a little tricky. We need to call the AuiManager’s AddPane() method to tell the AuiManager that we want it to “manage” something (in this case, the notebook). We also pass in a second argument which looks kind of confusing:

aui.AuiPaneInfo().Name("notebook_content").CenterPane().PaneBorder(False))

This parameter tells the AuiManager what to do with the notebook. In this case, we are telling it that the pane’s (i.e the notebook’s) name is “notebook_content”, which is what we use to look up the pane. We’re also telling the AuiManager that we want the pane to be in the centered dock position and the PaneBorder(False) command tells the AuiManager that we want a hidden border drawn around the notebook pane.

Our next example will be more complex and will show you how to change a few notebook settings.

Listing 5

import panelOne, panelTwo, panelThree
import wx
import wx.lib.agw.aui as aui
 
ID_NotebookArtGloss = 0
ID_NotebookArtSimple = 1
ID_NotebookArtVC71 = 2
ID_NotebookArtFF2 = 3
ID_NotebookArtVC8 = 4
ID_NotebookArtChrome = 5
 
########################################################################
class AUIManager(aui.AuiManager):
    """
    AUI Manager class
    """
 
    #----------------------------------------------------------------------
    def __init__(self, managed_window):
        """Constructor"""
        aui.AuiManager.__init__(self)
        self.SetManagedWindow(managed_window)
 
########################################################################
class AUINotebook(aui.AuiNotebook):
    """
    AUI Notebook class
    """
 
    #----------------------------------------------------------------------
    def __init__(self, parent):
        """Constructor"""
        aui.AuiNotebook.__init__(self, parent=parent)
        self.default_style = aui.AUI_NB_DEFAULT_STYLE | aui.AUI_NB_TAB_EXTERNAL_MOVE | wx.NO_BORDER
        self.SetWindowStyleFlag(self.default_style)
 
        # add some pages to the notebook
        pages = [panelOne, panelTwo, panelThree]
 
        x = 1
        for page in pages:
            label = "Tab #%i" % x
            tab = page.TabPanel(self)
            self.AddPage(tab, label, False)
            x += 1
 
########################################################################
class DemoFrame(wx.Frame):
    """
    wx.Frame class
    """
    #----------------------------------------------------------------------
    def __init__(self):
        """Constructor"""
        title = "AGW AUI Notebook Feature Tutorial"
        wx.Frame.__init__(self, None, wx.ID_ANY, 
                          title=title, size=(600,400))
        self.themeDict = {"Glossy Theme (Default)":0,
                          "Simple Theme":1,
                          "VC71 Theme":2,
                          "Firefox 2 Theme":3,
                          "VC8 Theme":4,
                          "Chrome Theme":5,
                          }
 
        # create the AUI manager
        self.aui_mgr = AUIManager(self)
 
        # create the AUI Notebook
        self.notebook = AUINotebook(self)
 
        self._notebook_style = self.notebook.default_style
 
        # add notebook to AUI manager
        self.aui_mgr.AddPane(self.notebook, 
                             aui.AuiPaneInfo().Name("notebook_content").
                             CenterPane().PaneBorder(False)) 
        self.aui_mgr.Update()
 
        # create menu and tool bars
        self.createMenu()
        self.createTB()
 
    #----------------------------------------------------------------------
    def createMenu(self):
        """
        Create the menu
        """
        def doBind(item, handler):
            """ Create menu events. """
            self.Bind(wx.EVT_MENU, handler, item)
 
        menubar = wx.MenuBar()
 
        fileMenu = wx.Menu()
 
        doBind( fileMenu.Append(wx.ID_ANY, "&Exit\tAlt+F4", 
                                "Exit Program"),self.onExit)
 
        optionsMenu = wx.Menu()
 
        doBind( optionsMenu.Append(wx.ID_ANY, 
                                   "Disable Current Tab"),
                self.onDisableTab)
 
        # add the menus to the menubar
        menubar.Append(fileMenu, "File")
        menubar.Append(optionsMenu, "Options")
 
        self.SetMenuBar(menubar)
 
    #----------------------------------------------------------------------
    def createTB(self):
        """
        Create the toolbar
        """
        TBFLAGS = ( wx.TB_HORIZONTAL
                    | wx.NO_BORDER
                    | wx.TB_FLAT )
        tb = self.CreateToolBar(TBFLAGS)
        keys = self.themeDict.keys()
        keys.sort()
        choices = keys
        cb = wx.ComboBox(tb, wx.ID_ANY, "Glossy Theme (Default)", 
                         choices=choices,
                         size=wx.DefaultSize,
                         style=wx.CB_DROPDOWN)
        cb.Bind(wx.EVT_COMBOBOX, self.onChangeTheme)
        tb.AddControl(cb)
        tb.AddSeparator()
 
        self.closeChoices = ["No Close Button", "Close Button At Right",
                             "Close Button On All Tabs",
                             "Close Button On Active Tab"]
        cb = wx.ComboBox(tb, wx.ID_ANY, 
                         self.closeChoices[3],
                         choices=self.closeChoices,
                         size=wx.DefaultSize, 
                         style=wx.CB_DROPDOWN)
        cb.Bind(wx.EVT_COMBOBOX, self.onChangeTabClose)
        tb.AddControl(cb)
 
        tb.Realize()
 
    #----------------------------------------------------------------------
    def onChangeTabClose(self, event):
        """
        Change how the close button behaves on a tab
 
        Note: Based partially on the agw AUI demo
        """
        choice = event.GetString()        
        self._notebook_style &= ~(aui.AUI_NB_CLOSE_BUTTON |
                                 aui.AUI_NB_CLOSE_ON_ACTIVE_TAB |
                                 aui.AUI_NB_CLOSE_ON_ALL_TABS)
 
        # note that this close button doesn't work for some reason
        if choice == "Close Button At Right":
            self._notebook_style ^= aui.AUI_NB_CLOSE_BUTTON 
        elif choice == "Close Button On All Tabs":
            self._notebook_style ^= aui.AUI_NB_CLOSE_ON_ALL_TABS 
        elif choice == "Close Button On Active Tab":
            self._notebook_style ^= aui.AUI_NB_CLOSE_ON_ACTIVE_TAB
 
        self.notebook.SetWindowStyleFlag(self._notebook_style)
        self.notebook.Refresh()
        self.notebook.Update()
 
    #----------------------------------------------------------------------
    def onChangeTheme(self, event):
        """
        Changes the notebook's theme
 
        Note: Based partially on the agw AUI demo
        """
 
        print event.GetString()
        evId = self.themeDict[event.GetString()]
        print evId
 
        all_panes = self.aui_mgr.GetAllPanes()
 
        for pane in all_panes:
 
            if isinstance(pane.window, aui.AuiNotebook):            
                nb = pane.window
 
                if evId == ID_NotebookArtGloss:
 
                    nb.SetArtProvider(aui.AuiDefaultTabArt())
                    self._notebook_theme = 0
 
                elif evId == ID_NotebookArtSimple:
                    nb.SetArtProvider(aui.AuiSimpleTabArt())
                    self._notebook_theme = 1
 
                elif evId == ID_NotebookArtVC71:
                    nb.SetArtProvider(aui.VC71TabArt())
                    self._notebook_theme = 2
 
                elif evId == ID_NotebookArtFF2:
                    nb.SetArtProvider(aui.FF2TabArt())
                    self._notebook_theme = 3
 
                elif evId == ID_NotebookArtVC8:
                    nb.SetArtProvider(aui.VC8TabArt())
                    self._notebook_theme = 4
 
                elif evId == ID_NotebookArtChrome:
                    nb.SetArtProvider(aui.ChromeTabArt())
                    self._notebook_theme = 5
 
                #nb.SetWindowStyleFlag(self._notebook_style)
                nb.Refresh()
                nb.Update()
 
    #----------------------------------------------------------------------
    def onDisableTab(self, event):
        """
        Disables the current tab
        """
        page = self.notebook.GetCurrentPage()
        page_idx = self.notebook.GetPageIndex(page)
 
        self.notebook.EnableTab(page_idx, False)
        self.notebook.AdvanceSelection()
 
    #----------------------------------------------------------------------
    def onExit(self, event):
        """
        Close the demo
        """
        self.Close()
 
 
#----------------------------------------------------------------------
if __name__ == "__main__":
    app = wx.PySimpleApp()
    frame = DemoFrame()
    frame.Show()
    app.MainLoop()

For this demo, I decided to try subclassing AuiManager and the aui.AuiNotebook. While I think this could be helpful if you ever needed to instantiate multiple AuiManager instances, for the purposes of this demo, it really didn’t help much other than showing you how to do it. Let’s unpack this example bit by bit and see how it works!

In the AuiManager class, I force the programmer to pass in the window to be managed and it calls the SetManagedWindow() automatically. You could do this with some of AuiManager’s other functions as well. In the AuiNotebook’s case, I set a default style using its SetWindowStyleFlag() method and then I add some pages to the notebook. This gives me a quick and easy way to create multiple notebooks quickly.

The DemoFrame does the bulk of the work. It creates a theme dictionary for later use, instantiates the AuiManager and AuiNotebook, and creates a toolbar and menubar. Our focus will be the event handlers related to the menubar and the toolbar as they affect the way the AuiNotebook functions. Our first method of interest is onChangeTabClose().

Listing 6

def onChangeTabClose(self, event):
    """
    Change how the close button behaves on a tab
 
    Note: Based partially on the agw AUI demo
    """
    choice = event.GetString()        
    self._notebook_style &= ~(aui.AUI_NB_CLOSE_BUTTON |
                             aui.AUI_NB_CLOSE_ON_ACTIVE_TAB |
                             aui.AUI_NB_CLOSE_ON_ALL_TABS)
 
    # note that this close button doesn't work for some reason
    if choice == "Close Button At Right":
        self._notebook_style ^= aui.AUI_NB_CLOSE_BUTTON 
    elif choice == "Close Button On All Tabs":
        self._notebook_style ^= aui.AUI_NB_CLOSE_ON_ALL_TABS 
    elif choice == "Close Button On Active Tab":
        self._notebook_style ^= aui.AUI_NB_CLOSE_ON_ACTIVE_TAB
 
    self.notebook.SetWindowStyleFlag(self._notebook_style)
    self.notebook.Refresh()
    self.notebook.Update()

This event handler is invoked from combobox events generated by the second combobox in the toolbar. Its purpose is to decide the placement of the close button on the tabs. First, it grabs the user’s choice by calling “event.GetString()”. Next it uses some bitwise operators to clear the close button related styles. If I’m reading it correctly, it “ands” the current notebook style with a “notted” multi-“or”. Yes, it’s confusing. To put it simply, it says that the three styles (aui.AUI_NB_CLOSE_BUTTON, aui.AUI_NB_CLOSE_ON_ACTIVE_TAB, aui.AUI_NB_CLOSE_ON_ALL_TABS) will be subtracted from the current notebook style.

Then I use a conditional to decide which style to actually apply to the notebook. Once that’s added to the variable, I use the notebook’s SetWindowStyleFlag() to apply it and then Refresh and Update the display so the user can see the changes.

Now we turn to changing the notebook’s style:

Listing 7

def onChangeTheme(self, event):
    """
    Changes the notebook's theme
 
    Note: Based partially on the agw AUI demo
    """
    evId = self.themeDict[event.GetString()]
    all_panes = self.aui_mgr.GetAllPanes()
 
    for pane in all_panes:
 
        if isinstance(pane.window, aui.AuiNotebook):
            nb = pane.window
 
            if evId == ID_NotebookArtGloss:
 
                nb.SetArtProvider(aui.AuiDefaultTabArt())
 
            elif evId == ID_NotebookArtSimple:
                nb.SetArtProvider(aui.AuiSimpleTabArt())
 
            elif evId == ID_NotebookArtVC71:
                nb.SetArtProvider(aui.VC71TabArt())
 
            elif evId == ID_NotebookArtFF2:
                nb.SetArtProvider(aui.FF2TabArt())
 
            elif evId == ID_NotebookArtVC8:
                nb.SetArtProvider(aui.VC8TabArt())
 
            elif evId == ID_NotebookArtChrome:
                nb.SetArtProvider(aui.ChromeTabArt())
 
            nb.Refresh()
            nb.Update()

The event handler is called from the first toolbar’s combobox events. It too grabs the user’s choice via event.GetString() and then uses the string as a key for my theme dictionary. The dictionary returns an integer which is assigned to “evId”. Next, the AuiManager instance calls GetAllPanes() to get a list of all the pages in the notebook. Finally, the handler then loops over the pages and uses a nested conditional to change the notebook’s them the call to SetArtProvider(). To show the changes, we finish by calling the notebook’s Refresh and Update methods.

The last method that I’m going to go over from this demo is “onDisableTab”:

Listing 8

def onDisableTab(self, event):
    """
    Disables the current tab
    """
    page = self.notebook.GetCurrentPage()
    page_idx = self.notebook.GetPageIndex(page)
 
    self.notebook.EnableTab(page_idx, False)
    self.notebook.AdvanceSelection()

This event handler gets fired by a menu event and is a pretty simple piece of code. First, we call the notebook’s GetCurrentPage() method and then pass the result to the notebook’s GetPageIndex() method. Now that we have the page index, we can use that to disable it via the notebook’s EnableTab method. As you can see, by passing False, we disable the page. You can also use the EnableTab method to re-enable the tab by passing True.

Wrapping Up

There are tons of other methods that affect the behavior of both of these notebooks. It would take several more articles to cover everything. Be sure to download the wxPython demo and the SVN version of the code to get the most of these wonderful notebooks and to see what I haven’t covered here. For example, I didn’t talk about the events of the respective widgets, the tab position (bottom, top, etc), or the many and varied abilities that can lock down the AuiNotebook. Also note that the AuiNotebook supports “perspectives”. The official demo has an example, so I didn’t replicate it here. Remember that both the FlatNotebook and the AGW AuiNotebook are pure python, so you can hack at them yourself if you know python.

Note: All code tested on Windowx XP / Vista with wxPython 2.8.10.1 (unicode) and Python 2.5 using the latest SVN versions of AGW. The code should work equally well on other operating systems. If not, let me know of email the wxPython mailing list.

Further Reading

Downloads


A Brief ConfigObj Tutorial

$
0
0

Python comes with a handy module called ConfigParser. It’s good for creating and reading configuration files (aka INI files). However, Michael Foord (author of IronPython in Action) and Nicola Larosa decided to write their own configuration module called ConfigObj. In many ways, it is an improvement over the standard library’s module. When I first looked at ConfigObj’s home page, I thought it was well documented, but it didn’t seem to have any fully functional snippets. Since I learn from docs plus examples, I found it harder to get started using ConfigObj when the examples were unavailable. When I started writing this article, I was unaware that Michael Foord has already written his own tutorial on the subject; but I had made a promise that I would write my own, so that is what you will get to read today!

Getting Started

First of all you will need to download ConfigObj. Once you have that downloaded and installed, we can continue. Got it? Then let’s see what it can do!

To start off, open a text editor and create a file with some contents like this:


product = Sony PS3
accessories = controller, eye, memory stick
# This is a comment that will be ignored
retail_price = $400

Save it where ever you like. I’m going to call mine “config.ini”. Now let’s see how ConfigObj can be used to extract that information:


>>> from configobj import ConfigObj
>>> config = ConfigObj(r"path to config.ini")
>>> config["product"]
'Sony PS3'
>>> config["accessories"]
['controller', 'eye', 'memory stick']
>>> type(config["accessories"])
<type 'list'>

As you can see, ConfigObj uses Python’s dict API to access the information it has extracted. All you had to do to get ConfigObj to parse the file was to pass the file’s path to ConfigObj. Now, if the information had been under a section (i.e. [Sony]), then you would have had to do pre-pend everything with [“Sony”], like this: config[“Sony”][“product”]. Also take note that the “accessories” section was returned as a list of strings. ConfigObj will take any valid line with a comma-separated list and return it as a Python list. You can also create multi-line strings in the config file as long as you enclose them with triple single or double quotes.

If you need to create a sub-section in the file, then use extra square brackets. For example, [Sony] is the top section, [[Playstation]] is the sub-section and [[[PS3]]] is the sub-section of the sub-section. You can create sub-sections up to any depth. For more information on the formatting of the file, I recommend the documentation linked to above.

Now we’ll do the reverse and create the config file programmatically.

import configobj
 
def createConfig(path):
    config = configobj.ConfigObj()
    config.filename = path
    config["Sony"] = {}
    config["Sony"]["product"] = "Sony PS3"
    config["Sony"]["accessories"] = ['controller', 'eye', 'memory stick']
    config["Sony"]["retail price"] = "$400"
    config.write()

As you can see, all it took was 8 lines of code. In the code above, we create a function and pass it the path for our config file. Then we create a ConfigObj object and set its filename property. To create the section, we create an empty dict with the name “Sony”. Then we pre-pend each line of the sections contents in the same way. Finally, we call our config object’s write method to write the data to the file.

Using a configspec

ConfigObj also provides a way to validate your configuration files using a configspec. When I mentioned that I was going to write this article, Steven Sproat (creator of Whyteboard) volunteered his configspec code as an example. I took his specification and used it to create a default config file. In this example, we use Foord’s validate module to do the validation. I don’t think it’s included in your ConfigObj download, so you may need to download it as well. Now, let’s take a look at the code:

import configobj, validate
 
cfg = """
bmp_select_transparent = boolean(default=False)
canvas_border = integer(min=10, max=35, default=15)
colour1 = list(min=3, max=3, default=list('280', '0', '0'))
colour2 = list(min=3, max=3, default=list('255', '255', '0'))
colour3 = list(min=3, max=3, default=list('0', '255', '0'))
colour4 = list(min=3, max=3, default=list('255', '0', '0'))
colour5 = list(min=3, max=3, default=list('0', '0', '255'))
colour6 = list(min=3, max=3, default=list('160', '32', '240'))
colour7 = list(min=3, max=3, default=list('0', '255', '255'))
colour8 = list(min=3, max=3, default=list('255', '165', '0'))
colour9 = list(min=3, max=3, default=list('211', '211', '211'))
convert_quality = option('highest', 'high', 'normal', default='normal')
default_font = string
default_width = integer(min=1, max=12000, default=640)
default_height = integer(min=1, max=12000, default=480)
imagemagick_path = string
handle_size = integer(min=3, max=15, default=6)
language = option('English', 'English (United Kingdom)', 'Russian', 'Hindi', default='English')
print_title = boolean(default=True)
statusbar = boolean(default=True)
toolbar = boolean(default=True)
toolbox = option('icon', 'text', default='icon')
undo_sheets = integer(min=5, max=50, default=10)
"""
 
def createConfig(path):
    """
    Create a config file using a configspec
    and validate it against a Validator object
    """
    spec = cfg.split("\n")
    config = configobj.ConfigObj(path, configspec=spec)
    validator = validate.Validator()
    config.validate(validator, copy=True)
    config.filename = path
    config.write()
 
if __name__ == "__main__":
    createConfig("config.ini")

If you go and look at Steven’s original configspec, you’ll notice I shortened his list of languages quite a bit. I did this to make the code easier to read. Anyway, the configspec allows the programmer the ability to specify what types are returned for each line in the configuration file. It also can be used to set a default value and a min and max values (among other things). If you run the code above, you will see a “config.ini” file generated in the current working directory that has just the default values. If the programmer didn’t specify a default, then that line isn’t even added to the configuration.

Let’s take a closer look at what’s going on just to make sure you understand. In the createConfig function, we create a ConfigObj instance by passing in the file path and setting the configspec. Note that the configspec can also be a normal text file or a python file rather than the string that is in this example. Next, we create a Validator object. Normal usage is to just call config.validate(validator), but in this code I set the copy argument to True so that I could create a file. Otherwise, all it would do is validate that the file I passed in fit the configspec’s rules. Finally I set the config’s filename and write the data out.

Wrapping Up

Now you know just enough to get you started on the ins and outs of ConfigObj. I hope you’ll find it as helpful as I have. There’s lots more to learn, so be sure to check out some of the links below.

Note: All code tested on Windows XP with Python 2.5, ConfigObj 4.6.0, and Validate 1.0.0.

Further Reading

Download the Source

Enabling Screen Locking with Python

$
0
0

A few months ago, my employer needed to lock down some of our workstations to be compliant with some new software we were installing from another government organization. We needed to force those machines to lock after so many minutes elapsed and we needed to make it such that the user could not change those settings. In this article, you’ll find out how do this and as a bonus, I’ll also show you how to lock your Windows machine on demand with Python.

Hacking the Registry to Lock the Machine

To start, we’ll take a look at my original script and then we’ll refactor it a bit to make the code better:

from _winreg import CreateKey, SetValueEx
from _winreg import HKEY_CURRENT_USER, HKEY_USERS
from _winreg import REG_DWORD, REG_SZ
 
try:
    i = 0
    while True:
        subkey = EnumKey(HKEY_USERS, i)
        if len(subkey) > 30:
            break
        i += 1
except WindowsError:
    # WindowsError: [Errno 259] No more data is available
    # looped through all the subkeys without finding the right one
    raise WindowsError("Could not apply workstation lock settings!")
 
keyOne = CreateKey(HKEY_USERS, r'%s\Control Panel\Desktop' % subkey)
keyTwo = CreateKey(HKEY_CURRENT_USER, r'Software\Microsoft\Windows\CurrentVersion\Policies\System')
 
# enable screen saver security
SetValueEx(keyOne, 'ScreenSaverIsSecure', 0, REG_DWORD, 1)
# set screen saver timeout
SetValueEx(keyOne, 'ScreenSaveTimeOut', 0, REG_SZ, '420')
# set screen saver
SetValueEx(keyOne, 'SCRNSAVE.EXE', 0, REG_SZ, 'logon.scr')
# disable screen saver tab
SetValueEx(keyTwo, 'NoDispScrSavPage', 0, REG_DWORD, 1)
 
CloseKey(keyOne)
CloseKey(keyTwo)

It took a while to discover this, but to set the right key, we need to find the first sub-key that is larger than 30 characters in length under the HKEY_USERS hive. I’m sure there’s probably a better way to do this, but I haven’t found it yet. Anyway, once we’ve found the long key, we break out of the loop and open the keys we need or create them if they don’t already exist. This is the reason that we use CreateKey since it will do just that. Next, we set four values and then we close the keys to apply the new settings. You can read the comments to see what each key does. Now let’s refine the code a bit to make it into a function:

from _winreg import *
 
def modifyRegistry(key, sub_key, valueName, valueType, value):
    """
    A simple function used to change values in
    the Windows Registry.
    """
    try:
        key_handle = OpenKey(key, sub_key, 0, KEY_ALL_ACCESS)
    except WindowsError:
        key_handle = CreateKey(key, sub_key)
 
    SetValueEx(key_handle, valueName, 0, valueType, value)
    CloseKey(key_handle)
 
try:
    i = 0
    while True:
        subkey = EnumKey(HKEY_USERS, i)
        if len(subkey) > 30:
            break
        i += 1
except WindowsError:
    # WindowsError: [Errno 259] No more data is available
    # looped through all the subkeys without finding the right one
    raise WindowsError("Could not apply workstation lock settings!")
 
subkey = r'%s\Control Panel\Desktop' % subkey
data= [('ScreenSaverIsSecure', REG_DWORD, 1),
              ('ScreenSaveTimeOut', REG_SZ, '420'),
              ('SCRNSAVE.EXE', REG_SZ, 'logon.scr')]
 
for valueName, valueType, value in data:
    modifyRegistry(HKEY_USERS, subkey, valueName, 
                   valueType, value)
 
modifyRegistry(HKEY_CURRENT_USER,
               r'Software\Microsoft\Windows\CurrentVersion\Policies\System',
               'NoDispScrSavPage', REG_DWORD, 1)

As you can see, first we import everything in the _winreg module. This isn’t really recommended as you can accidentally overwrite functions that you’ve imported, which is why this is sometimes called “poisoning the namespace”. However, almost every example I’ve ever seen that uses the _winreg modules does it that way. See the first example for the correct way to import from it.

Next, we create a general purpose function that can open the key, or create the key if it’s not already there. The function will also set the value and close the key for us. After that, we do basically the same thing that we did in the previous example: we loop over the HKEY_USERS hive and break appropriately. To mix things up a bit, we create a data variable that holds a list of tuples. We loop over that and call our function with the appropriate parameters and for good measure, we demonstrate how to call it outside of a loop.

Locking the Machine Programmatically

Now you may be thinking that we already covered how to lock the machine programmatically. Well, we did in a sense; but what we really did was set up a timer to lock the machine sometime in the future when the machine has been idle. What if we want to lock the machine now? Some of you are probably thinking we should just hit the Windows key plus “L” and that is a good idea. However, the reason I created this script is because I have to remotely connect to my machine with VNC from time-to-time and I need to go through multiple steps to lock the machine when using VNC whereas if you have Python set up correctly, you can just double-click a script file and have it do the locking for you. That’s what this little script does:

import os
 
winpath = os.environ["windir"]
os.system(winpath + r'\system32\rundll32 user32.dll, LockWorkStation')

This three line script imports the os module, grabs the Windows directory using its environ method and then calls os.system to lock the machine. If you were to open a DOS window on your machine and type the following into it, you would have the exact same effect:


C:\windows\system32\rundll32 user32.dll, LockWorkStation

Wrapping Up

Now you know how to lock your machine with Python. If you put the first example in a login script, then you can use it lock down some or all the machines on your network. This is very handy if you have users that like to wander off or go to lots of meetings, but leave their machines logged in. It protects them from snooping and can protect your company from espionage.

Using Python to Reduce the Roaming Profile

$
0
0

Roaming Profiles are a blessing and a curse. If the user uses the internet, their browser’s cached files will grow like mad. If the user downloads programs to their desktop or creates large Powerpoint files anywhere in their profile, then they have to be managed whenever the user logs in or out. There are several solutions to this problem: disk quotas, blocking the ability to download or put stuff in one’s profile, etc. In this article, I will show you how to exclude specific directories from the user’s profile using Python.

This is basically just a Windows Registry hack. As always, be sure to back up your Registry before applying any changes to it in case something goes horribly awry and you make your machine unbootable.

from _winreg import *
 
try:
    key = OpenKey(HKEY_CURRENT_USER, r"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon",
                  0, KEY_ALL_ACCESS)
except WindowsError:
    key = CreateKey(HKEY_CURRENT_USER, r"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon")
 
# Exclude directories from roaming profile 
prof_dirs = "Local Settings;Temporary Internet Files;History;Temp;My Documents;Recent"
SetValueEx(key, "ExcludeProfileDirs", 0, REG_SZ, prof_dirs)     
CloseKey(key)

This code is pretty simple. First we imported the various modules and constants from _winreg. Then we tried to open the appropriate Registry key and created it if the key didn’t already exist. Next we created a string of semi-colon delimited directories to exclude from the roaming profile. Finally, we set the appropriate value and closed the key.

And that’s all there is to this simple script!

Getting Photo Metadata (EXIF) Using Python

$
0
0

Last week, I was trying to find out how to get my photo’s metadata. I had noticed that Windows could display the camera model, creation date and lots of other data on my photos, but I couldn’t remember what that data was called. I finally found what I was looking for. The term is EXIF (Exchangeable Image File Format). For this post, we’ll take a look at the various 3rd party packages that give you access to this information.

My first thought was that the Python Imaging Library would have this functionality, but I hadn’t found the EXIF term yet and couldn’t find that info in PIL’s handbook without it. Fortunately, I did eventually find a way to use PIL via a stackoverflow thread. Here’s the method that it showed:

from PIL import Image
from PIL.ExifTags import TAGS
 
def get_exif(fn):
    ret = {}
    i = Image.open(fn)
    info = i._getexif()
    for tag, value in info.items():
        decoded = TAGS.get(tag, tag)
        ret[decoded] = value
    return ret

This works quite well and returns a nice dictionary object. There are several fields that I found useless, such as the “MakerNote” field which looked like a lot of hexadecimal values, so you’ll probably only want to use certain pieces of data. Here’s a sample of some of the info I got back:

{'YResolution': (180, 1), 
 'ResolutionUnit': 2, 
 'Make': 'Canon', 
 'Flash': 16, 
 'DateTime': '2009:09:11 11:29:10', 
 'MeteringMode': 5, 
 'XResolution': (180, 1), 
 'ColorSpace': 1, 
 'ExifImageWidth': 3264, 
 'DateTimeDigitized': '2009:09:11 11:29:10', 
 'ApertureValue': (116, 32), 
 'FocalPlaneYResolution': (2448000, 169), 
 'CompressedBitsPerPixel': (3, 1), 
 'SensingMethod': 2, 
 'FNumber': (35, 10), 
 'DateTimeOriginal': '2009:09:11 11:29:10', 
 'FocalLength': (26000, 1000), 
 'FocalPlaneXResolution': (3264000, 225), 
 'ExifOffset': 196, 
 'ExifImageHeight': 2448, 
 'ISOSpeedRatings': 100, 
 'Model': 'Canon PowerShot S5 IS', 
 'Orientation': 1, 
 'ExposureTime': (1, 200), 
 'FileSource': '\x03', 
 'MaxApertureValue': (116, 32), 
 'ExifInteroperabilityOffset': 3346, 
 'FlashPixVersion': '0100', 
 'FocalPlaneResolutionUnit': 2, 
 'YCbCrPositioning': 1, 
 'ExifVersion': '0220'}

I don’t really know what all of those values mean, but I know I can use some of them. My purpose for wanting the data is to expand my simple Image Viewer such that it can display more info to the user about their photo.

Here are a few other libraries I found that can supposedly give access to the EXIF data:

I tried the Python Exif Parser and it worked quite well. When I tried to install pyexiv2 on my Python 2.5 box at work, I got an error message about Python 2.6 not being found and then the installer quit. There is no mention on the pyexiv2 website that it requires a certain version of Python to work, so that was a little frustrating. Most of these modules have little or no documentation, which was also pretty frustrating. From what I can tell, EXIF.py is supposed to be used via the command line rather than as an importable module.

Anyway, back to the Python Exif Parser. It’s actually simpler to use than PIL is. Here’s all you need to do after copying the exif.py file into your Python path:

import exif
photo_path = "somePath\to\a\photo.jpg"
data = exif.parse(photo_path)

The code above returns mostly the same information that the PIL snippet does, although it uses integers instead of hex for the “MakersNote” and it has several “Tag0xa406′” fields whereas the PIL data had some numerical fields (which I excluded above). I assume they reference the same information in different ways though.

Anyway, should you find yourself wandering the web when trying to discover this information, hopefully you will stumble upon this post and it will point you in the right direction.

Parsing ID3 Tags from MP3s using Python

$
0
0

While working on my Python mp3 player I realized I needed to research what Python had to offer for parsing ID3 tags. There are tons of projects out there, but most of them appear to be either dead, don’t have documentation or both. In this post, you will discover the wild world of MP3 tag parsing in Python along with me and we’ll see if we can find something that I can use to enhance my mp3 player project.

For this exercise, we’ll try to get the following information from our parsers:

  • Artist
  • Title of Album
  • Track Title
  • Length of Track
  • Album Release Date

We’ll probably need more metadata than that, but this is the usual stuff I care about in my mp3 playing experience. We will look at the following 3rd party libraries to see how they hold up:

Let’s get started!

Can Mutagen Save the Day?

One of the reasons to include Mutagen in this round up is because it supports ASF, FLAC, M4A, Monkey’s Audio, Musepack, Ogg FLAC, Ogg Speex, Ogg Theora, Ogg Vorbis, True Audio, WavPack and OptimFROG in addition to MP3 parsing. Thus, we could potentially expand our MP3 player quite a bit. I was pretty excited when I found this package. However, while the package appears to be actively developed, the documentation is almost non-existent. If you are a new Python programmer, you will find this library difficult to just jump into and use.

To install Mutagen, you’ll need to unpack it and navigate to its folder using the command line. Then execute the following:


python setup.py install

You may be able to use easy_install or pip as well, although their website doesn’t really say one way or the other. Now comes the fun part: trying to figure out how to use the module without documentation! Fortunately, I found a blog post that gave me some clues. From what I gather, Mutagen follows the ID3 specification pretty closely, so rather than abstracting it so that you would have functions like GetArtist, you actually end up reading ID3 text frames and use their terminology. Thus, TPE1 = Artist (or Lead Singer), TIT2 = Title, etc. Let’s look at an example:

>>> path = r'D:\mp3\12 Stones\2002 - 12 Stones\01 - Crash.mp3'
>>> from mutagen.id3 import ID3
>>> audio = ID3(path)
>>> audio
>>> audio['TPE1']
TPE1(encoding=0, text=[u'12 Stones'])
>>> audio['TPE1'].text
[u'12 Stones']

Here’s a more proper example:

from mutagen.id3 import ID3
 
#----------------------------------------------------------------------
def getMutagenTags(path):
    """"""
    audio = ID3(path)
 
    print "Artist: %s" % audio['TPE1'].text[0]
    print "Track: %s" % audio["TIT2"].text[0]
    print "Release Year: %s" % audio["TDRC"].text[0]

I personally find this to be difficult to read and use, so I won’t be using this module for my mp3 player unless I need to add additional digital file formats to it. Also note that I wasn’t able to figure out how to get the track’s play length or album title. Let’s move on to our next ID3 parser and see how it fares.

eyeD3

If you go to eyeD3’s website, you’ll notice that it doesn’t seem to support Windows. This is a problem for many users and almost caused me to drop it from this round-up. Fortunately, I found a forum that mentioned a way to make it work. The idea was to rename the “setup.py.in” file in the main folder to just “setup.py” and the “__init__.py.in” file to “__init__.py”, which you’ll find in “src\eyeD3”. Then you can install it using the usual “python setup.py install”. Once you have it installed, it’s really easy to use. Check out the following function:

import eyeD3
 
#----------------------------------------------------------------------
def getEyeD3Tags(path):
    """"""
    trackInfo = eyeD3.Mp3AudioFile(path)
    tag = trackInfo.getTag()
    tag.link(path)
 
    print "Artist: %s" % tag.getArtist()
    print "Album: %s" % tag.getAlbum()
    print "Track: %s" % tag.getTitle()
    print "Track Length: %s" % trackInfo.getPlayTimeString()
    print "Release Year: %s" % tag.getYear()

This package does meet our arbitrary requirements. The only regrettable aspect of the package is its lack of official Windows support. We’ll reserve judgment until after we’ve tried out our third possibility though.

Ned Batchelder’s id3reader.py

This module is probably the easiest of the three to install since it’s just one file. All you need to do is download it and put the file into the site-packages or somewhere else on your Python path. The primary problem of this parser is that Batchelder no longer supports it. Let’s see if there’s an easy way to get the information that we need.

import id3reader
 
#----------------------------------------------------------------------
def getTags(path):
    """"""
    id3r = id3reader.Reader(path)
 
    print "Artist: %s" % id3r.getValue('performer')
    print "Album: %s" % id3r.getValue('album')
    print "Track: %s" % id3r.getValue('title')
    print "Release Year: %s" % id3r.getValue('year')

Well, I didn’t see an obvious way to get the track length with this module without knowing the ID3 specification. Alas! While I like the simplicity and power of this module, the lack of support and a super simple API makes me reject it in favor of eyeD3. For now, that will be my library of choice for my mp3 player. If you know of a great ID3 parsing script, feel free to drop me a line in the comments. I saw others listed on Google as well, but quite a few of them were just as dead as Batchelder’s was.

The "Book" Controls of wxPython (Part 2 of 2)

$
0
0

In the first part of this series, I wrote on all the non-agw notebook widgets included with wxPython. For this second article, I will be focusing on the two notebooks that are in the AGW library of wxPython. AGW stands for Advanced Generic Widgets, a set of widgets that are written in Python instead of wrapped C++ code. I personally think that AGW is also a callback to its amazing author, Andrea Gavana. Regardless, the two widgets in this review will be the FlatNotebook and another AUI Notebook. The FlatNotebook has a great demo and I will spend most of this article on demos I've created that are based on it. The AUI Notebook is a part of agw.aui. While the demo for agw.aui is cool, it focuses on AUI in general, not the notebook. So I'll just show you what I can glean from that. Now, let's get cracking!

Update: The API changed slightly when it comes to AGW-related widgets. Basically some style flags in wxPython 2.8.11.0+ now require the agw-specific style flags. To use them, you'll need to use the agwStyle keyword. See Andrea's docs for more info: http://xoomer.virgilio.it/infinity77/AGW_Docs/ If you run into an error, try changing that first or post to the mailing list.

The Amazing FlatNotebook

The Flatbook control is written in Python rather than a wrapped widget from wxWidgets. It was added to wxPython with the release of wxPython 2.8.9.2 on February 16, 2009. Since then Andrea Gavana has been updating the agw library with lots of fixes. My examples will work with the 2.8.9.2+ versions of wxPython, but I recommend getting the SVN version of agw and replacing your default one with it as there have been a lot of bug fixes applied to the AUI module and several others. There is also an effort going on currently to better document this library in the code itself, so you may find that helpful too!

Here are a few of the FlatNotebook's Features:

  • 5 Different tab styles
  • It's a generic control (i.e. pure python) so it's easy to modify
  • You can use the mouse's middle-click to close tabs
  • A built-in function to add right-click pop-up menus on tabs
  • A way to hide the "X" that closes the individual tabs
  • Support for disabled tabs
  • Plus lots more! See the source and the wxPython Demo for more information!

Now that we've done an unpaid commercial, let's take a look at the actual product:

flatnotebookDemo

Listing 1

import panelOne, panelTwo, panelThree
import wx
import wx.lib.agw.flatnotebook as fnb

########################################################################
class FlatNotebookDemo(fnb.FlatNotebook):
    """
    Flatnotebook class
    """

    #----------------------------------------------------------------------
    def __init__(self, parent):
        """Constructor"""
        fnb.FlatNotebook.__init__(self, parent, wx.ID_ANY)
        
        pageOne = panelOne.TabPanel(self)
        pageTwo = panelTwo.TabPanel(self)
        pageThree = panelThree.TabPanel(self)
        
        self.AddPage(pageOne, "PageOne")
        self.AddPage(pageTwo, "PageTwo")
        self.AddPage(pageThree, "PageThree")  
    
########################################################################
class DemoFrame(wx.Frame):
    """
    Frame that holds all other widgets
    """

    #----------------------------------------------------------------------
    def __init__(self):
        """Constructor"""        
        wx.Frame.__init__(self, None, wx.ID_ANY, 
                          "FlatNotebook Tutorial",
                          size=(600,400)
                          )
        panel = wx.Panel(self)
        
        notebook = FlatNotebookDemo(panel)
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(notebook, 1, wx.ALL|wx.EXPAND, 5)
        panel.SetSizer(sizer)
        self.Layout()
        
        self.Show()
        
#----------------------------------------------------------------------
if __name__ == "__main__":
    app = wx.PySimpleApp()
    frame = DemoFrame()
    app.MainLoop()

In Listing 1, I subclass FlatNotebook and use the generic panels from my previous article for the pages. You'll notice that FlatNotebook has its own AddPage method that mimics the wx.Notebook. This should come as no surprise as the FlatNotebook's API is such that you should be able to use it as a drop-in replacement for wx.Notebook. Of course, right out of the box, FlatNotebook has the advantage. If you run the demo above, you'll see that FlatNotebook allows the user to rearrange the tabs, close the tabs and it includes some previous/next buttons in case you have more tabs than can fit on-screen at once.

Now let's take a look at the various styles that we can apply to FlatNotebook:

flatnotebookStyleDemo

Listing 2

import panelOne, panelTwo, panelThree
import wx
import wx.lib.agw.flatnotebook as fnb

########################################################################
class FlatNotebookDemo(fnb.FlatNotebook):
    """
    Flatnotebook class
    """

    #----------------------------------------------------------------------
    def __init__(self, parent):
        """Constructor"""
        fnb.FlatNotebook.__init__(self, parent, wx.ID_ANY)
        
        pageOne = panelOne.TabPanel(self)
        pageTwo = panelTwo.TabPanel(self)
        pageThree = panelThree.TabPanel(self)
        
        self.AddPage(pageOne, "PageOne")
        self.AddPage(pageTwo, "PageTwo")
        self.AddPage(pageThree, "PageThree")  
    

########################################################################
class DemoFrame(wx.Frame):
    """
    Frame that holds all other widgets
    """

    #----------------------------------------------------------------------
    def __init__(self):
        """Constructor"""        
        wx.Frame.__init__(self, None, wx.ID_ANY, 
                          "FlatNotebook Tutorial with Style",
                          size=(600,400)
                          )
        self.styleDict = {"Default":self.OnDefaultStyle,
                          "VC71":self.OnVC71Style,
                          "VC8":self.OnVC8Style,
                          "Fancy":self.OnFancyStyle,
                          "Firefox 2":self.OnFF2Style}
        choices = self.styleDict.keys()
                          
        panel = wx.Panel(self)        
        self.notebook = FlatNotebookDemo(panel)
        self.styleCbo = wx.ComboBox(panel, wx.ID_ANY, "Default",
                                    wx.DefaultPosition, wx.DefaultSize,
                                    choices=choices, style=wx.CB_DROPDOWN)
        styleBtn = wx.Button(panel, wx.ID_ANY, "Change Style")
        styleBtn.Bind(wx.EVT_BUTTON, self.onStyle)
        
        # create some sizers
        sizer = wx.BoxSizer(wx.VERTICAL)
        hSizer = wx.BoxSizer(wx.HORIZONTAL)
        
        # add the widgets to the sizers
        sizer.Add(self.notebook, 1, wx.ALL|wx.EXPAND, 5)
        hSizer.Add(self.styleCbo, 0, wx.ALL|wx.CENTER, 5)
        hSizer.Add(styleBtn, 0, wx.ALL, 5)
        sizer.Add(wx.StaticLine(panel), 0, wx.ALL|wx.EXPAND, 5)
        sizer.Add(hSizer, 0, wx.ALL, 5)
        
        panel.SetSizer(sizer)
        self.Layout()
        
        self.Show()
        
    #----------------------------------------------------------------------
    def onStyle(self, event):
        """
        Changes the style of the tabs
        """
        print "in onStyle"
        style = self.styleCbo.GetValue()
        print style
        self.styleDict[style]()           
        
    # The following methods were taken from the wxPython 
    # demo for the FlatNotebook
    def OnFF2Style(self):
        
        style = self.notebook.GetWindowStyleFlag()

        # remove old tabs style
        mirror = ~(fnb.FNB_VC71 | fnb.FNB_VC8 | fnb.FNB_FANCY_TABS | fnb.FNB_FF2)
        style &= mirror

        style |= fnb.FNB_FF2

        self.notebook.SetWindowStyleFlag(style)


    def OnVC71Style(self):

        style = self.notebook.GetWindowStyleFlag()

        # remove old tabs style
        mirror = ~(fnb.FNB_VC71 | fnb.FNB_VC8 | fnb.FNB_FANCY_TABS | fnb.FNB_FF2)
        style &= mirror

        style |= fnb.FNB_VC71

        self.notebook.SetWindowStyleFlag(style)


    def OnVC8Style(self):

        style = self.notebook.GetWindowStyleFlag()

        # remove old tabs style
        mirror = ~(fnb.FNB_VC71 | fnb.FNB_VC8 | fnb.FNB_FANCY_TABS | fnb.FNB_FF2)
        style &= mirror

        # set new style
        style |= fnb.FNB_VC8

        self.notebook.SetWindowStyleFlag(style)


    def OnDefaultStyle(self):

        style = self.notebook.GetWindowStyleFlag()

        # remove old tabs style
        mirror = ~(fnb.FNB_VC71 | fnb.FNB_VC8 | fnb.FNB_FANCY_TABS | fnb.FNB_FF2)
        style &= mirror

        self.notebook.SetWindowStyleFlag(style)


    def OnFancyStyle(self):

        style = self.notebook.GetWindowStyleFlag()

        # remove old tabs style
        mirror = ~(fnb.FNB_VC71 | fnb.FNB_VC8 | fnb.FNB_FANCY_TABS | fnb.FNB_FF2)
        style &= mirror

        style |= fnb.FNB_FANCY_TABS
        self.notebook.SetWindowStyleFlag(style)
        
#----------------------------------------------------------------------
if __name__ == "__main__":
    app = wx.PySimpleApp()
    frame = DemoFrame()
    app.MainLoop()

That's a lot of code for a "simple" example, but I think it will help us understand how to apply tab styles to our widget. I borrowed most of the methods from the wxPython demo, in case you didn't notice. The primary talking point in this code is the contents of those methods, which are mostly the same. Here's the main snippet to take away from this section:

style = self.notebook.GetWindowStyleFlag()

# remove old tabs style
mirror = ~(fnb.FNB_VC71 | fnb.FNB_VC8 | fnb.FNB_FANCY_TABS | fnb.FNB_FF2)
style &= mirror
style |= fnb.FNB_FF2
self.notebook.SetWindowStyleFlag(style)

First, we need to get the current style of the FlatNotebook. Then we use some fancy magic in the "mirror" line that creates a set of styles that we want to remove. The line, "style &= mirror" actually does the removing and then we add the style we wanted with "style |= fnb.FNB_FF2". Finally, we use SetWindowStyleFlag() to actually apply the style to the widget. You may be wondering what's up with all those goofy symbols (i.e. |, ~, &). Well, those are known as bitwise operators. I don't use them much myself, so I recommend reading the Python documentation for full details as I don't fully understand them myself.

For my next demo, I created a way to add and delete pages from the FlatNotebook. Let's see how:

flatnotebookPageDemo

Listing 3

import panelOne, panelTwo, panelThree
import random
import wx
import wx.lib.agw.flatnotebook as fnb

########################################################################
class FlatNotebookDemo(fnb.FlatNotebook):
    """
    Flatnotebook class
    """
    #----------------------------------------------------------------------
    def __init__(self, parent):
        """Constructor"""
        fnb.FlatNotebook.__init__(self, parent, wx.ID_ANY)
        
########################################################################
class DemoFrame(wx.Frame):
    """
    Frame that holds all other widgets
    """

    #----------------------------------------------------------------------
    def __init__(self, title="FlatNotebook Add/Remove Page Tutorial"):
        """Constructor"""        
        wx.Frame.__init__(self, None, wx.ID_ANY, 
                          title=title,
                          size=(600,400)
                          )
        self._newPageCounter = 0
        panel = wx.Panel(self)
        self.createRightClickMenu()
        
        # create some widgets
        self.notebook = FlatNotebookDemo(panel)
        addPageBtn = wx.Button(panel, label="Add Page")
        addPageBtn.Bind(wx.EVT_BUTTON, self.onAddPage)
        removePageBtn = wx.Button(panel, label="Remove Page")
        removePageBtn.Bind(wx.EVT_BUTTON, self.onDeletePage)
        self.notebook.SetRightClickMenu(self._rmenu)
        
        # create some sizers
        sizer = wx.BoxSizer(wx.VERTICAL)
        btnSizer = wx.BoxSizer(wx.HORIZONTAL)
        
        # layout the widgets
        sizer.Add(self.notebook, 1, wx.ALL|wx.EXPAND, 5)
        btnSizer.Add(addPageBtn, 0, wx.ALL, 5)
        btnSizer.Add(removePageBtn, 0, wx.ALL, 5)
        sizer.Add(btnSizer)
        panel.SetSizer(sizer)
        self.Layout()
        
        self.Show()
        
    #----------------------------------------------------------------------
    def createRightClickMenu(self):
        """
        Based on method from flatnotebook demo
        """
        self._rmenu = wx.Menu()
        item = wx.MenuItem(self._rmenu, wx.ID_ANY, 
                           "Close Tab\tCtrl+F4", 
                           "Close Tab")
        self.Bind(wx.EVT_MENU, self.onDeletePage, item)
        self._rmenu.AppendItem(item)
        
    #----------------------------------------------------------------------
    def onAddPage(self, event):
        """
        This method is based on the flatnotebook demo
        
        It adds a new page to the notebook
        """
        caption = "New Page Added #" + str(self._newPageCounter)
        self.Freeze()

        self.notebook.AddPage(self.createPage(caption), caption, True)
        self.Thaw()
        self._newPageCounter = self._newPageCounter + 1
        
    #----------------------------------------------------------------------
    def createPage(self, caption):
        """
        Creates a notebook page from one of three
        panels at random and returns the new page
        """
        panel_list = [panelOne, panelTwo, panelThree]
        obj = random.choice(panel_list)
        page = obj.TabPanel(self.notebook)
        return page
        
    #----------------------------------------------------------------------
    def onDeletePage(self, event):
        """
        This method is based on the flatnotebook demo
        
        It removes a page from the notebook
        """
        self.notebook.DeletePage(self.notebook.GetSelection())
        
#----------------------------------------------------------------------
if __name__ == "__main__":
    app = wx.PySimpleApp()
    frame = DemoFrame()
    app.MainLoop()

The code above allows the user to add as many pages as they want by clicking the Add Page button. The Remove Page button will remove whatever page is currently selected. When adding a page, but button handler freezes the frame and calls the notebook's AddPage method. This calls the "createPage" method which randomly grabs one of my pre-defined panels, instantiates it and returns it to the AddPage method. On returning to the "onAddPage" method, the frame is thawed and the page counter is incremented.

The Remove Page button calls the notebook's GetSelection() method to get the currently selected tab and then calls the notebook's DeletePage() method to remove it from the notebook.

Another fun functionality that I enabled was the tab right-click menu, which gives us another way to close a tab, although you could use to do other actions as well. All you need to do to enable it is to call the notebook's SetRightClickMenu() method and pass in a wx.Menu object.

There are tons of other features for you to explore as well. Be sure to check out the FlatNotebook demo in the official wxPython demo where you can learn to close tabs with the middle mouse button or via double-clicks, turn on gradient colors for the tab background, disable tabs, enable smart tabbing (which is kind of like the alt+tab menu in Windows), create drag-and-drop tabs between notebooks and much, much more!

AGW AUI Notebook

agwAuiNotebookDemo

Andrea Gavana went to the trouble of creating a pure python version of the Advanced User Interface (AUI) that provides perspective saving, floating sub-windows that can be docked, customizable look and feel and the splittable AUI Notebook. His notebook will be the focus of this section. The AGW AUI Notebook has lots of features, but I'm just going to go over some of the basics. If you want to see all the features, be sure to read the code and check out the demo in the official wxPython Demo. As I mentioned at the beginning of this tutorial, be sure to download the latest version of AUI (or AGW as a whole) from SVN to get all the bug fixes.

Let's take a look at the simple example I used for the screenshot above:

Listing 4

#----------------------------------------------------------------------
# agwAUINotebook.py
#
# Created: December 2009
#
# Author: Mike Driscoll - mike@pythonlibrary.org
#
# Note: Some code comes from the wxPython demo
#
#----------------------------------------------------------------------


import wx
import wx.lib.agw.aui as aui 

########################################################################
class TabPanelOne(wx.Panel):
    """
    A simple wx.Panel class
    """
    #----------------------------------------------------------------------
    def __init__(self, parent):
        """"""
        wx.Panel.__init__(self, parent=parent, id=wx.ID_ANY)
        
        sizer = wx.BoxSizer(wx.VERTICAL)
        txtOne = wx.TextCtrl(self, wx.ID_ANY, "")
        txtTwo = wx.TextCtrl(self, wx.ID_ANY, "")
        
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(txtOne, 0, wx.ALL, 5)
        sizer.Add(txtTwo, 0, wx.ALL, 5)
        
        self.SetSizer(sizer)
        
########################################################################
class DemoFrame(wx.Frame):
    """
    wx.Frame class
    """
    
    #----------------------------------------------------------------------
    def __init__(self):
        wx.Frame.__init__(self, None, wx.ID_ANY, 
                          "AGW AUI Notebook Tutorial",
                          size=(600,400))
 
        self._mgr = aui.AuiManager()
        
        # tell AuiManager to manage this frame
        self._mgr.SetManagedWindow(self)
                
        notebook = aui.AuiNotebook(self)
        panelOne = TabPanelOne(notebook)
        panelTwo = TabPanelOne(notebook)
        
        notebook.AddPage(panelOne, "PanelOne", False)
        notebook.AddPage(panelTwo, "PanelTwo", False)
        
        self._mgr.AddPane(notebook, 
                          aui.AuiPaneInfo().Name("notebook_content").
                          CenterPane().PaneBorder(False)) 
        self._mgr.Update()
        #notebook.EnableTab(1, False)
        
 #----------------------------------------------------------------------
# Run the program
if __name__ == "__main__":
    app = wx.PySimpleApp()
    frame = DemoFrame()
    frame.Show()
    app.MainLoop()

The first difference between this notebook and the original AuiNotebook is that this one requires an AuiManager object. It may be that something similar is behind the original as well, but that's hidden from us. Anyway, the first step is instantiating the AuiManager and then giving it the frame to manage via its SetManagedWindow() method. Now we can add the AUI Notebook. Note that we pass the frame as the parent of the notebook instead of the AuiManager. I think the reason is that when the AuiManager is given the frame, it becomes the top level window.

The next part of the equation should look familiar: AddPage(). Let's see what it accepts:

AddPage(self, page, caption, select=False, bitmap=wx.NullBitmap, disabled_bitmap=wx.NullBitmap, control=None)

In my code, I only pass in the first three parameters, but you can also add a couple bitmaps and a wx.Window for the control. The next bit is a little tricky. We need to call the AuiManager's AddPane() method to tell the AuiManager that we want it to "manage" something (in this case, the notebook). We also pass in a second argument which looks kind of confusing:

aui.AuiPaneInfo().Name("notebook_content").CenterPane().PaneBorder(False)) 

This parameter tells the AuiManager what to do with the notebook. In this case, we are telling it that the pane's (i.e the notebook's) name is "notebook_content", which is what we use to look up the pane. We're also telling the AuiManager that we want the pane to be in the centered dock position and the PaneBorder(False) command tells the AuiManager that we want a hidden border drawn around the notebook pane.

Our next example will be more complex and will show you how to change a few notebook settings.

Listing 5

import panelOne, panelTwo, panelThree
import wx
import wx.lib.agw.aui as aui

ID_NotebookArtGloss = 0
ID_NotebookArtSimple = 1
ID_NotebookArtVC71 = 2
ID_NotebookArtFF2 = 3
ID_NotebookArtVC8 = 4
ID_NotebookArtChrome = 5

########################################################################
class AUIManager(aui.AuiManager):
    """
    AUI Manager class
    """

    #----------------------------------------------------------------------
    def __init__(self, managed_window):
        """Constructor"""
        aui.AuiManager.__init__(self)
        self.SetManagedWindow(managed_window)

########################################################################
class AUINotebook(aui.AuiNotebook):
    """
    AUI Notebook class
    """

    #----------------------------------------------------------------------
    def __init__(self, parent):
        """Constructor"""
        aui.AuiNotebook.__init__(self, parent=parent)
        self.default_style = aui.AUI_NB_DEFAULT_STYLE | aui.AUI_NB_TAB_EXTERNAL_MOVE | wx.NO_BORDER
        self.SetWindowStyleFlag(self.default_style)

        # add some pages to the notebook
        pages = [panelOne, panelTwo, panelThree]

        x = 1
        for page in pages:
            label = "Tab #%i" % x
            tab = page.TabPanel(self)
            self.AddPage(tab, label, False)
            x += 1

########################################################################
class DemoFrame(wx.Frame):
    """
    wx.Frame class
    """
    #----------------------------------------------------------------------
    def __init__(self):
        """Constructor"""
        title = "AGW AUI Notebook Feature Tutorial"
        wx.Frame.__init__(self, None, wx.ID_ANY, 
                          title=title, size=(600,400))
        self.themeDict = {"Glossy Theme (Default)":0,
                          "Simple Theme":1,
                          "VC71 Theme":2,
                          "Firefox 2 Theme":3,
                          "VC8 Theme":4,
                          "Chrome Theme":5,
                          }

        # create the AUI manager
        self.aui_mgr = AUIManager(self)

        # create the AUI Notebook
        self.notebook = AUINotebook(self)
        
        self._notebook_style = self.notebook.default_style
                
        # add notebook to AUI manager
        self.aui_mgr.AddPane(self.notebook, 
                             aui.AuiPaneInfo().Name("notebook_content").
                             CenterPane().PaneBorder(False)) 
        self.aui_mgr.Update()
        
        # create menu and tool bars
        self.createMenu()
        self.createTB()
        
    #----------------------------------------------------------------------
    def createMenu(self):
        """
        Create the menu
        """
        def doBind(item, handler):
            """ Create menu events. """
            self.Bind(wx.EVT_MENU, handler, item)
        
        menubar = wx.MenuBar()
        
        fileMenu = wx.Menu()
        
        doBind( fileMenu.Append(wx.ID_ANY, "&Exit\tAlt+F4", 
                                "Exit Program"),self.onExit)
        
        optionsMenu = wx.Menu()
        
        doBind( optionsMenu.Append(wx.ID_ANY, 
                                   "Disable Current Tab"),
                self.onDisableTab)
        
        # add the menus to the menubar
        menubar.Append(fileMenu, "File")
        menubar.Append(optionsMenu, "Options")
        
        self.SetMenuBar(menubar)
        
    #----------------------------------------------------------------------
    def createTB(self):
        """
        Create the toolbar
        """
        TBFLAGS = ( wx.TB_HORIZONTAL
                    | wx.NO_BORDER
                    | wx.TB_FLAT )
        tb = self.CreateToolBar(TBFLAGS)
        keys = self.themeDict.keys()
        keys.sort()
        choices = keys
        cb = wx.ComboBox(tb, wx.ID_ANY, "Glossy Theme (Default)", 
                         choices=choices,
                         size=wx.DefaultSize,
                         style=wx.CB_DROPDOWN)
        cb.Bind(wx.EVT_COMBOBOX, self.onChangeTheme)
        tb.AddControl(cb)
        tb.AddSeparator()
        
        self.closeChoices = ["No Close Button", "Close Button At Right",
                             "Close Button On All Tabs",
                             "Close Button On Active Tab"]
        cb = wx.ComboBox(tb, wx.ID_ANY, 
                         self.closeChoices[3],
                         choices=self.closeChoices,
                         size=wx.DefaultSize, 
                         style=wx.CB_DROPDOWN)
        cb.Bind(wx.EVT_COMBOBOX, self.onChangeTabClose)
        tb.AddControl(cb)
        
        tb.Realize()
        
    #----------------------------------------------------------------------
    def onChangeTabClose(self, event):
        """
        Change how the close button behaves on a tab
        
        Note: Based partially on the agw AUI demo
        """
        choice = event.GetString()        
        self._notebook_style &= ~(aui.AUI_NB_CLOSE_BUTTON |
                                 aui.AUI_NB_CLOSE_ON_ACTIVE_TAB |
                                 aui.AUI_NB_CLOSE_ON_ALL_TABS)
        
        # note that this close button doesn't work for some reason
        if choice == "Close Button At Right":
            self._notebook_style ^= aui.AUI_NB_CLOSE_BUTTON 
        elif choice == "Close Button On All Tabs":
            self._notebook_style ^= aui.AUI_NB_CLOSE_ON_ALL_TABS 
        elif choice == "Close Button On Active Tab":
            self._notebook_style ^= aui.AUI_NB_CLOSE_ON_ACTIVE_TAB
            
        self.notebook.SetWindowStyleFlag(self._notebook_style)
        self.notebook.Refresh()
        self.notebook.Update()
        
    #----------------------------------------------------------------------
    def onChangeTheme(self, event):
        """
        Changes the notebook's theme
        
        Note: Based partially on the agw AUI demo
        """
                
        print event.GetString()
        evId = self.themeDict[event.GetString()]
        print evId
        
        all_panes = self.aui_mgr.GetAllPanes()
        
        for pane in all_panes:

            if isinstance(pane.window, aui.AuiNotebook):            
                nb = pane.window

                if evId == ID_NotebookArtGloss:
                
                    nb.SetArtProvider(aui.AuiDefaultTabArt())
                    self._notebook_theme = 0
                
                elif evId == ID_NotebookArtSimple:
                    nb.SetArtProvider(aui.AuiSimpleTabArt())
                    self._notebook_theme = 1
                
                elif evId == ID_NotebookArtVC71:
                    nb.SetArtProvider(aui.VC71TabArt())
                    self._notebook_theme = 2
                    
                elif evId == ID_NotebookArtFF2:
                    nb.SetArtProvider(aui.FF2TabArt())
                    self._notebook_theme = 3

                elif evId == ID_NotebookArtVC8:
                    nb.SetArtProvider(aui.VC8TabArt())
                    self._notebook_theme = 4

                elif evId == ID_NotebookArtChrome:
                    nb.SetArtProvider(aui.ChromeTabArt())
                    self._notebook_theme = 5

                #nb.SetWindowStyleFlag(self._notebook_style)
                nb.Refresh()
                nb.Update()
                
    #----------------------------------------------------------------------
    def onDisableTab(self, event):
        """
        Disables the current tab
        """
        page = self.notebook.GetCurrentPage()
        page_idx = self.notebook.GetPageIndex(page)
        
        self.notebook.EnableTab(page_idx, False)
        self.notebook.AdvanceSelection()
        
    #----------------------------------------------------------------------
    def onExit(self, event):
        """
        Close the demo
        """
        self.Close()
        

#----------------------------------------------------------------------
if __name__ == "__main__":
    app = wx.PySimpleApp()
    frame = DemoFrame()
    frame.Show()
    app.MainLoop()

For this demo, I decided to try subclassing AuiManager and the aui.AuiNotebook. While I think this could be helpful if you ever needed to instantiate multiple AuiManager instances, for the purposes of this demo, it really didn't help much other than showing you how to do it. Let's unpack this example bit by bit and see how it works!

In the AuiManager class, I force the programmer to pass in the window to be managed and it calls the SetManagedWindow() automatically. You could do this with some of AuiManager's other functions as well. In the AuiNotebook's case, I set a default style using its SetWindowStyleFlag() method and then I add some pages to the notebook. This gives me a quick and easy way to create multiple notebooks quickly.

The DemoFrame does the bulk of the work. It creates a theme dictionary for later use, instantiates the AuiManager and AuiNotebook, and creates a toolbar and menubar. Our focus will be the event handlers related to the menubar and the toolbar as they affect the way the AuiNotebook functions. Our first method of interest is onChangeTabClose().

Listing 6

def onChangeTabClose(self, event):
    """
    Change how the close button behaves on a tab
    
    Note: Based partially on the agw AUI demo
    """
    choice = event.GetString()        
    self._notebook_style &= ~(aui.AUI_NB_CLOSE_BUTTON |
                             aui.AUI_NB_CLOSE_ON_ACTIVE_TAB |
                             aui.AUI_NB_CLOSE_ON_ALL_TABS)
    
    # note that this close button doesn't work for some reason
    if choice == "Close Button At Right":
        self._notebook_style ^= aui.AUI_NB_CLOSE_BUTTON 
    elif choice == "Close Button On All Tabs":
        self._notebook_style ^= aui.AUI_NB_CLOSE_ON_ALL_TABS 
    elif choice == "Close Button On Active Tab":
        self._notebook_style ^= aui.AUI_NB_CLOSE_ON_ACTIVE_TAB
        
    self.notebook.SetWindowStyleFlag(self._notebook_style)
    self.notebook.Refresh()
    self.notebook.Update()

This event handler is invoked from combobox events generated by the second combobox in the toolbar. Its purpose is to decide the placement of the close button on the tabs. First, it grabs the user's choice by calling "event.GetString()". Next it uses some bitwise operators to clear the close button related styles. If I'm reading it correctly, it "ands" the current notebook style with a "notted" multi-"or". Yes, it's confusing. To put it simply, it says that the three styles (aui.AUI_NB_CLOSE_BUTTON, aui.AUI_NB_CLOSE_ON_ACTIVE_TAB, aui.AUI_NB_CLOSE_ON_ALL_TABS) will be subtracted from the current notebook style.

Then I use a conditional to decide which style to actually apply to the notebook. Once that's added to the variable, I use the notebook's SetWindowStyleFlag() to apply it and then Refresh and Update the display so the user can see the changes.

Now we turn to changing the notebook's style:

Listing 7

def onChangeTheme(self, event):
    """
    Changes the notebook's theme
    
    Note: Based partially on the agw AUI demo
    """
    evId = self.themeDict[event.GetString()]
    all_panes = self.aui_mgr.GetAllPanes()
    
    for pane in all_panes:

        if isinstance(pane.window, aui.AuiNotebook):
            nb = pane.window

            if evId == ID_NotebookArtGloss:
            
                nb.SetArtProvider(aui.AuiDefaultTabArt())
                
            elif evId == ID_NotebookArtSimple:
                nb.SetArtProvider(aui.AuiSimpleTabArt())
                
            elif evId == ID_NotebookArtVC71:
                nb.SetArtProvider(aui.VC71TabArt())
                
            elif evId == ID_NotebookArtFF2:
                nb.SetArtProvider(aui.FF2TabArt())
                
            elif evId == ID_NotebookArtVC8:
                nb.SetArtProvider(aui.VC8TabArt())
                
            elif evId == ID_NotebookArtChrome:
                nb.SetArtProvider(aui.ChromeTabArt())
                
            nb.Refresh()
            nb.Update()

The event handler is called from the first toolbar's combobox events. It too grabs the user's choice via event.GetString() and then uses the string as a key for my theme dictionary. The dictionary returns an integer which is assigned to "evId". Next, the AuiManager instance calls GetAllPanes() to get a list of all the pages in the notebook. Finally, the handler then loops over the pages and uses a nested conditional to change the notebook's them the call to SetArtProvider(). To show the changes, we finish by calling the notebook's Refresh and Update methods.

The last method that I'm going to go over from this demo is "onDisableTab":

Listing 8

def onDisableTab(self, event):
    """
    Disables the current tab
    """
    page = self.notebook.GetCurrentPage()
    page_idx = self.notebook.GetPageIndex(page)
    
    self.notebook.EnableTab(page_idx, False)
    self.notebook.AdvanceSelection()

This event handler gets fired by a menu event and is a pretty simple piece of code. First, we call the notebook's GetCurrentPage() method and then pass the result to the notebook's GetPageIndex() method. Now that we have the page index, we can use that to disable it via the notebook's EnableTab method. As you can see, by passing False, we disable the page. You can also use the EnableTab method to re-enable the tab by passing True.

Wrapping Up

There are tons of other methods that affect the behavior of both of these notebooks. It would take several more articles to cover everything. Be sure to download the wxPython demo and the SVN version of the code to get the most of these wonderful notebooks and to see what I haven't covered here. For example, I didn't talk about the events of the respective widgets, the tab position (bottom, top, etc), or the many and varied abilities that can lock down the AuiNotebook. Also note that the AuiNotebook supports "perspectives". The official demo has an example, so I didn't replicate it here. Remember that both the FlatNotebook and the AGW AuiNotebook are pure python, so you can hack at them yourself if you know python.

Note: All code tested on Windowx XP / Vista with wxPython 2.8.10.1 (unicode) and Python 2.5 using the latest SVN versions of AGW. The code should work equally well on other operating systems. If not, let me know of email the wxPython mailing list.

Further Reading

Downloads

The post The "Book" Controls of wxPython (Part 2 of 2) appeared first on Mouse Vs Python.

A Brief ConfigObj Tutorial

$
0
0

Python comes with a handy module called ConfigParser. It's good for creating and reading configuration files (aka INI files). However, Michael Foord (author of IronPython in Action) and Nicola Larosa decided to write their own configuration module called ConfigObj. In many ways, it is an improvement over the standard library's module. When I first looked at ConfigObj's home page, I thought it was well documented, but it didn't seem to have any fully functional snippets. Since I learn from docs plus examples, I found it harder to get started using ConfigObj when the examples were unavailable. When I started writing this article, I was unaware that Michael Foord has already written his own tutorial on the subject; but I had made a promise that I would write my own, so that is what you will get to read today!

Getting Started

First of all you will need to download ConfigObj. Once you have that downloaded and installed, we can continue. Got it? Then let's see what it can do!

To start off, open a text editor and create a file with some contents like this:


product = Sony PS3
accessories = controller, eye, memory stick
# This is a comment that will be ignored
retail_price = $400

Save it where ever you like. I'm going to call mine "config.ini". Now let's see how ConfigObj can be used to extract that information:


>>> from configobj import ConfigObj
>>> config = ConfigObj(r"path to config.ini")
>>> config["product"]
'Sony PS3'
>>> config["accessories"]
['controller', 'eye', 'memory stick']
>>> type(config["accessories"])

As you can see, ConfigObj uses Python's dict API to access the information it has extracted. All you had to do to get ConfigObj to parse the file was to pass the file's path to ConfigObj. Now, if the information had been under a section (i.e. [Sony]), then you would have had to do pre-pend everything with ["Sony"], like this: config["Sony"]["product"]. Also take note that the "accessories" section was returned as a list of strings. ConfigObj will take any valid line with a comma-separated list and return it as a Python list. You can also create multi-line strings in the config file as long as you enclose them with triple single or double quotes.

If you need to create a sub-section in the file, then use extra square brackets. For example, [Sony] is the top section, [[Playstation]] is the sub-section and [[[PS3]]] is the sub-section of the sub-section. You can create sub-sections up to any depth. For more information on the formatting of the file, I recommend the documentation linked to above.

Now we'll do the reverse and create the config file programmatically.

import configobj

def createConfig(path):
    config = configobj.ConfigObj()
    config.filename = path
    config["Sony"] = {}
    config["Sony"]["product"] = "Sony PS3"
    config["Sony"]["accessories"] = ['controller', 'eye', 'memory stick']
    config["Sony"]["retail price"] = "$400"
    config.write()

As you can see, all it took was 8 lines of code. In the code above, we create a function and pass it the path for our config file. Then we create a ConfigObj object and set its filename property. To create the section, we create an empty dict with the name "Sony". Then we pre-pend each line of the sections contents in the same way. Finally, we call our config object's write method to write the data to the file.

Using a configspec

ConfigObj also provides a way to validate your configuration files using a configspec. When I mentioned that I was going to write this article, Steven Sproat (creator of Whyteboard) volunteered his configspec code as an example. I took his specification and used it to create a default config file. In this example, we use Foord's validate module to do the validation. I don't think it's included in your ConfigObj download, so you may need to download it as well. Now, let's take a look at the code:


import configobj, validate

cfg = """
bmp_select_transparent = boolean(default=False)
canvas_border = integer(min=10, max=35, default=15)
colour1 = list(min=3, max=3, default=list('280', '0', '0'))
colour2 = list(min=3, max=3, default=list('255', '255', '0'))
colour3 = list(min=3, max=3, default=list('0', '255', '0'))
colour4 = list(min=3, max=3, default=list('255', '0', '0'))
colour5 = list(min=3, max=3, default=list('0', '0', '255'))
colour6 = list(min=3, max=3, default=list('160', '32', '240'))
colour7 = list(min=3, max=3, default=list('0', '255', '255'))
colour8 = list(min=3, max=3, default=list('255', '165', '0'))
colour9 = list(min=3, max=3, default=list('211', '211', '211'))
convert_quality = option('highest', 'high', 'normal', default='normal')
default_font = string
default_width = integer(min=1, max=12000, default=640)
default_height = integer(min=1, max=12000, default=480)
imagemagick_path = string
handle_size = integer(min=3, max=15, default=6)
language = option('English', 'English (United Kingdom)', 'Russian', 'Hindi', default='English')
print_title = boolean(default=True)
statusbar = boolean(default=True)
toolbar = boolean(default=True)
toolbox = option('icon', 'text', default='icon')
undo_sheets = integer(min=5, max=50, default=10)
"""

def createConfig(path):
    """
    Create a config file using a configspec
    and validate it against a Validator object
    """
    spec = cfg.split("\n")
    config = configobj.ConfigObj(path, configspec=spec)
    validator = validate.Validator()
    config.validate(validator, copy=True)
    config.filename = path
    config.write()
    
if __name__ == "__main__":
    createConfig("config.ini")

If you go and look at Steven's original configspec, you'll notice I shortened his list of languages quite a bit. I did this to make the code easier to read. Anyway, the configspec allows the programmer the ability to specify what types are returned for each line in the configuration file. It also can be used to set a default value and a min and max values (among other things). If you run the code above, you will see a "config.ini" file generated in the current working directory that has just the default values. If the programmer didn't specify a default, then that line isn't even added to the configuration.

Let's take a closer look at what's going on just to make sure you understand. In the createConfig function, we create a ConfigObj instance by passing in the file path and setting the configspec. Note that the configspec can also be a normal text file or a python file rather than the string that is in this example. Next, we create a Validator object. Normal usage is to just call config.validate(validator), but in this code I set the copy argument to True so that I could create a file. Otherwise, all it would do is validate that the file I passed in fit the configspec's rules. Finally I set the config's filename and write the data out.

Wrapping Up

Now you know just enough to get you started on the ins and outs of ConfigObj. I hope you'll find it as helpful as I have. There's lots more to learn, so be sure to check out some of the links below.

Note: All code tested on Windows XP with Python 2.5, ConfigObj 4.6.0, and Validate 1.0.0.

Further Reading

Download the Source

The post A Brief ConfigObj Tutorial appeared first on Mouse Vs Python.


Enabling Screen Locking with Python

$
0
0

A few months ago, my employer needed to lock down some of our workstations to be compliant with some new software we were installing from another government organization. We needed to force those machines to lock after so many minutes elapsed and we needed to make it such that the user could not change those settings. In this article, you'll find out how do this and as a bonus, I'll also show you how to lock your Windows machine on demand with Python.

Hacking the Registry to Lock the Machine

To start, we'll take a look at my original script and then we'll refactor it a bit to make the code better:

from _winreg import CreateKey, SetValueEx
from _winreg import HKEY_CURRENT_USER, HKEY_USERS
from _winreg import REG_DWORD, REG_SZ

try:
    i = 0
    while True:
        subkey = EnumKey(HKEY_USERS, i)
        if len(subkey) > 30:
            break
        i += 1
except WindowsError:
    # WindowsError: [Errno 259] No more data is available
    # looped through all the subkeys without finding the right one
    raise WindowsError("Could not apply workstation lock settings!")

keyOne = CreateKey(HKEY_USERS, r'%s\Control Panel\Desktop' % subkey)
keyTwo = CreateKey(HKEY_CURRENT_USER, r'Software\Microsoft\Windows\CurrentVersion\Policies\System')

# enable screen saver security
SetValueEx(keyOne, 'ScreenSaverIsSecure', 0, REG_DWORD, 1)
# set screen saver timeout
SetValueEx(keyOne, 'ScreenSaveTimeOut', 0, REG_SZ, '420')
# set screen saver
SetValueEx(keyOne, 'SCRNSAVE.EXE', 0, REG_SZ, 'logon.scr')
# disable screen saver tab
SetValueEx(keyTwo, 'NoDispScrSavPage', 0, REG_DWORD, 1)

CloseKey(keyOne)
CloseKey(keyTwo)

It took a while to discover this, but to set the right key, we need to find the first sub-key that is larger than 30 characters in length under the HKEY_USERS hive. I'm sure there's probably a better way to do this, but I haven't found it yet. Anyway, once we've found the long key, we break out of the loop and open the keys we need or create them if they don't already exist. This is the reason that we use CreateKey since it will do just that. Next, we set four values and then we close the keys to apply the new settings. You can read the comments to see what each key does. Now let's refine the code a bit to make it into a function:

from _winreg import *

def modifyRegistry(key, sub_key, valueName, valueType, value):
    """
    A simple function used to change values in
    the Windows Registry.
    """
    try:
        key_handle = OpenKey(key, sub_key, 0, KEY_ALL_ACCESS)
    except WindowsError:
        key_handle = CreateKey(key, sub_key)
        
    SetValueEx(key_handle, valueName, 0, valueType, value)
    CloseKey(key_handle)
    
try:
    i = 0
    while True:
        subkey = EnumKey(HKEY_USERS, i)
        if len(subkey) > 30:
            break
        i += 1
except WindowsError:
    # WindowsError: [Errno 259] No more data is available
    # looped through all the subkeys without finding the right one
    raise WindowsError("Could not apply workstation lock settings!")

subkey = r'%s\Control Panel\Desktop' % subkey
data= [('ScreenSaverIsSecure', REG_DWORD, 1),
              ('ScreenSaveTimeOut', REG_SZ, '420'),
              ('SCRNSAVE.EXE', REG_SZ, 'logon.scr')]

for valueName, valueType, value in data:
    modifyRegistry(HKEY_USERS, subkey, valueName, 
                   valueType, value)

modifyRegistry(HKEY_CURRENT_USER,
               r'Software\Microsoft\Windows\CurrentVersion\Policies\System',
               'NoDispScrSavPage', REG_DWORD, 1)

As you can see, first we import everything in the _winreg module. This isn't really recommended as you can accidentally overwrite functions that you've imported, which is why this is sometimes called "poisoning the namespace". However, almost every example I've ever seen that uses the _winreg modules does it that way. See the first example for the correct way to import from it.

Next, we create a general purpose function that can open the key, or create the key if it's not already there. The function will also set the value and close the key for us. After that, we do basically the same thing that we did in the previous example: we loop over the HKEY_USERS hive and break appropriately. To mix things up a bit, we create a data variable that holds a list of tuples. We loop over that and call our function with the appropriate parameters and for good measure, we demonstrate how to call it outside of a loop.

Locking the Machine Programmatically

Now you may be thinking that we already covered how to lock the machine programmatically. Well, we did in a sense; but what we really did was set up a timer to lock the machine sometime in the future when the machine has been idle. What if we want to lock the machine now? Some of you are probably thinking we should just hit the Windows key plus "L" and that is a good idea. However, the reason I created this script is because I have to remotely connect to my machine with VNC from time-to-time and I need to go through multiple steps to lock the machine when using VNC whereas if you have Python set up correctly, you can just double-click a script file and have it do the locking for you. That's what this little script does:

import os

winpath = os.environ["windir"]
os.system(winpath + r'\system32\rundll32 user32.dll, LockWorkStation')

This three line script imports the os module, grabs the Windows directory using its environ method and then calls os.system to lock the machine. If you were to open a DOS window on your machine and type the following into it, you would have the exact same effect:


C:\windows\system32\rundll32 user32.dll, LockWorkStation

Wrapping Up

Now you know how to lock your machine with Python. If you put the first example in a login script, then you can use it lock down some or all the machines on your network. This is very handy if you have users that like to wander off or go to lots of meetings, but leave their machines logged in. It protects them from snooping and can protect your company from espionage.

The post Enabling Screen Locking with Python appeared first on Mouse Vs Python.

Using Python to Reduce the Roaming Profile

$
0
0

Roaming Profiles are a blessing and a curse. If the user uses the internet, their browser's cached files will grow like mad. If the user downloads programs to their desktop or creates large Powerpoint files anywhere in their profile, then they have to be managed whenever the user logs in or out. There are several solutions to this problem: disk quotas, blocking the ability to download or put stuff in one's profile, etc. In this article, I will show you how to exclude specific directories from the user's profile using Python.

This is basically just a Windows Registry hack. As always, be sure to back up your Registry before applying any changes to it in case something goes horribly awry and you make your machine unbootable.

from _winreg import *

try:
    key = OpenKey(HKEY_CURRENT_USER, r"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon",
                  0, KEY_ALL_ACCESS)
except WindowsError:
    key = CreateKey(HKEY_CURRENT_USER, r"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon")

# Exclude directories from roaming profile 
prof_dirs = "Local Settings;Temporary Internet Files;History;Temp;My Documents;Recent"
SetValueEx(key, "ExcludeProfileDirs", 0, REG_SZ, prof_dirs)     
CloseKey(key)

This code is pretty simple. First we imported the various modules and constants from _winreg. Then we tried to open the appropriate Registry key and created it if the key didn't already exist. Next we created a string of semi-colon delimited directories to exclude from the roaming profile. Finally, we set the appropriate value and closed the key.

And that's all there is to this simple script!

The post Using Python to Reduce the Roaming Profile appeared first on Mouse Vs Python.

Getting Photo Metadata (EXIF) Using Python

$
0
0

Last week, I was trying to find out how to get my photo's metadata. I had noticed that Windows could display the camera model, creation date and lots of other data on my photos, but I couldn't remember what that data was called. I finally found what I was looking for. The term is EXIF (Exchangeable Image File Format). For this post, we'll take a look at the various 3rd party packages that give you access to this information.

My first thought was that the Python Imaging Library would have this functionality, but I hadn't found the EXIF term yet and couldn't find that info in PIL's handbook without it. Fortunately, I did eventually find a way to use PIL via a stackoverflow thread. Here's the method that it showed:

from PIL import Image
from PIL.ExifTags import TAGS

def get_exif(fn):
    ret = {}
    i = Image.open(fn)
    info = i._getexif()
    for tag, value in info.items():
        decoded = TAGS.get(tag, tag)
        ret[decoded] = value
    return ret

This works quite well and returns a nice dictionary object. There are several fields that I found useless, such as the "MakerNote" field which looked like a lot of hexadecimal values, so you'll probably only want to use certain pieces of data. Here's a sample of some of the info I got back:

{'YResolution': (180, 1), 
 'ResolutionUnit': 2, 
 'Make': 'Canon', 
 'Flash': 16, 
 'DateTime': '2009:09:11 11:29:10', 
 'MeteringMode': 5, 
 'XResolution': (180, 1), 
 'ColorSpace': 1, 
 'ExifImageWidth': 3264, 
 'DateTimeDigitized': '2009:09:11 11:29:10', 
 'ApertureValue': (116, 32), 
 'FocalPlaneYResolution': (2448000, 169), 
 'CompressedBitsPerPixel': (3, 1), 
 'SensingMethod': 2, 
 'FNumber': (35, 10), 
 'DateTimeOriginal': '2009:09:11 11:29:10', 
 'FocalLength': (26000, 1000), 
 'FocalPlaneXResolution': (3264000, 225), 
 'ExifOffset': 196, 
 'ExifImageHeight': 2448, 
 'ISOSpeedRatings': 100, 
 'Model': 'Canon PowerShot S5 IS', 
 'Orientation': 1, 
 'ExposureTime': (1, 200), 
 'FileSource': '\x03', 
 'MaxApertureValue': (116, 32), 
 'ExifInteroperabilityOffset': 3346, 
 'FlashPixVersion': '0100', 
 'FocalPlaneResolutionUnit': 2, 
 'YCbCrPositioning': 1, 
 'ExifVersion': '0220'}

I don't really know what all of those values mean, but I know I can use some of them. My purpose for wanting the data is to expand my simple Image Viewer such that it can display more info to the user about their photo.

Here are a few other libraries I found that can supposedly give access to the EXIF data:

I tried the Python Exif Parser and it worked quite well. When I tried to install pyexiv2 on my Python 2.5 box at work, I got an error message about Python 2.6 not being found and then the installer quit. There is no mention on the pyexiv2 website that it requires a certain version of Python to work, so that was a little frustrating. Most of these modules have little or no documentation, which was also pretty frustrating. From what I can tell, EXIF.py is supposed to be used via the command line rather than as an importable module.

Anyway, back to the Python Exif Parser. It's actually simpler to use than PIL is. Here's all you need to do after copying the exif.py file into your Python path:

import exif
photo_path = "somePath\to\a\photo.jpg"
data = exif.parse(photo_path)

The code above returns mostly the same information that the PIL snippet does, although it uses integers instead of hex for the "MakersNote" and it has several "Tag0xa406'" fields whereas the PIL data had some numerical fields (which I excluded above). I assume they reference the same information in different ways though.

Anyway, should you find yourself wandering the web when trying to discover this information, hopefully you will stumble upon this post and it will point you in the right direction.

The post Getting Photo Metadata (EXIF) Using Python appeared first on Mouse Vs Python.

Parsing ID3 Tags from MP3s using Python

$
0
0

While working on my Python mp3 player I realized I needed to research what Python had to offer for parsing ID3 tags. There are tons of projects out there, but most of them appear to be either dead, don't have documentation or both. In this post, you will discover the wild world of MP3 tag parsing in Python along with me and we'll see if we can find something that I can use to enhance my mp3 player project.

For this exercise, we'll try to get the following information from our parsers:

  • Artist
  • Title of Album
  • Track Title
  • Length of Track
  • Album Release Date

We'll probably need more metadata than that, but this is the usual stuff I care about in my mp3 playing experience. We will look at the following 3rd party libraries to see how they hold up:

Let's get started!

Can Mutagen Save the Day?

One of the reasons to include Mutagen in this round up is because it supports ASF, FLAC, M4A, Monkey's Audio, Musepack, Ogg FLAC, Ogg Speex, Ogg Theora, Ogg Vorbis, True Audio, WavPack and OptimFROG in addition to MP3 parsing. Thus, we could potentially expand our MP3 player quite a bit. I was pretty excited when I found this package. However, while the package appears to be actively developed, the documentation is almost non-existent. If you are a new Python programmer, you will find this library difficult to just jump into and use.

To install Mutagen, you'll need to unpack it and navigate to its folder using the command line. Then execute the following:


python setup.py install

You may be able to use easy_install or pip as well, although their website doesn't really say one way or the other. Now comes the fun part: trying to figure out how to use the module without documentation! Fortunately, I found a blog post that gave me some clues. From what I gather, Mutagen follows the ID3 specification pretty closely, so rather than abstracting it so that you would have functions like GetArtist, you actually end up reading ID3 text frames and use their terminology. Thus, TPE1 = Artist (or Lead Singer), TIT2 = Title, etc. Let's look at an example:

>>> path = r'D:\mp3\12 Stones\2002 - 12 Stones\01 - Crash.mp3'
>>> from mutagen.id3 import ID3
>>> audio = ID3(path)
>>> audio
>>> audio['TPE1']
TPE1(encoding=0, text=[u'12 Stones'])
>>> audio['TPE1'].text
[u'12 Stones']

Here's a more proper example:

from mutagen.id3 import ID3

#----------------------------------------------------------------------
def getMutagenTags(path):
    """"""
    audio = ID3(path)
    
    print "Artist: %s" % audio['TPE1'].text[0]
    print "Track: %s" % audio["TIT2"].text[0]
    print "Release Year: %s" % audio["TDRC"].text[0]

I personally find this to be difficult to read and use, so I won't be using this module for my mp3 player unless I need to add additional digital file formats to it. Also note that I wasn't able to figure out how to get the track's play length or album title. Let's move on to our next ID3 parser and see how it fares.

eyeD3

If you go to eyeD3's website, you'll notice that it doesn't seem to support Windows. This is a problem for many users and almost caused me to drop it from this round-up. Fortunately, I found a forum that mentioned a way to make it work. The idea was to rename the "setup.py.in" file in the main folder to just "setup.py" and the "__init__.py.in" file to "__init__.py", which you'll find in "src\eyeD3". Then you can install it using the usual "python setup.py install". Once you have it installed, it's really easy to use. Check out the following function:

import eyeD3

#----------------------------------------------------------------------
def getEyeD3Tags(path):
    """"""
    trackInfo = eyeD3.Mp3AudioFile(path)
    tag = trackInfo.getTag()
    tag.link(path)
    
    print "Artist: %s" % tag.getArtist()
    print "Album: %s" % tag.getAlbum()
    print "Track: %s" % tag.getTitle()
    print "Track Length: %s" % trackInfo.getPlayTimeString()
    print "Release Year: %s" % tag.getYear()

This package does meet our arbitrary requirements. The only regrettable aspect of the package is its lack of official Windows support. We'll reserve judgment until after we've tried out our third possibility though.

Ned Batchelder's id3reader.py

This module is probably the easiest of the three to install since it's just one file. All you need to do is download it and put the file into the site-packages or somewhere else on your Python path. The primary problem of this parser is that Batchelder no longer supports it. Let's see if there's an easy way to get the information that we need.

import id3reader

#----------------------------------------------------------------------
def getTags(path):
    """"""
    id3r = id3reader.Reader(path)
    
    print "Artist: %s" % id3r.getValue('performer')
    print "Album: %s" % id3r.getValue('album')
    print "Track: %s" % id3r.getValue('title')
    print "Release Year: %s" % id3r.getValue('year')

Well, I didn't see an obvious way to get the track length with this module without knowing the ID3 specification. Alas! While I like the simplicity and power of this module, the lack of support and a super simple API makes me reject it in favor of eyeD3. For now, that will be my library of choice for my mp3 player. If you know of a great ID3 parsing script, feel free to drop me a line in the comments. I saw others listed on Google as well, but quite a few of them were just as dead as Batchelder's was.

The post Parsing ID3 Tags from MP3s using Python appeared first on Mouse Vs Python.

Viewing all 12 articles
Browse latest View live