FC48 Python编程

出自Full Circle 中文项目主页

跳转到: 导航, 搜索

纠正

在本系列的第21讲,我们将到如何构建一个界面PlaylistMaker.glade。为了统一期间,以后无论是代码还是文件名,我们都用全小写的playlistmaker.glade指代这个文件。以免不必要的麻烦。

在咱们开始之前,请先找出我们上个月教程中的playlistmaker.gladeplaylistmaker.py这两个源文件。我们这一期教程将从这两个文件开始,如果没有这两个文件,请到上期文章中去找一找。在动手写代码之前,我们先看看播放列表文件是个神马样子。目前,不同的播放器有很很多不同格式的播放列表文件规范。我们的例子用的是比较简单的*.m3u这类播放列表格式。他本身就是一个一#EXTM3U开头的纯文本文档。后面依次添加包括完整的路径各首歌曲的信息。可选的,在各歌曲信息之前,可选添加这首歌的长度、封面、作者、音轨、歌曲名等等信息。我们先来看一个简单的不包含这些扩展信息的基本版本的M3U播放列表文件范例:

#EXTM3U 
Adult Contemporary/Chris Rea/Collection/02 - On The Beach.mp3 
Adult Contemporary/Chris Rea/Collection/07 - Fool (If You Think It's Over).mp3 
Adult Contemporary/Chris Rea/Collection/11 - Looking For The Summer.mp3

所有的路径都是相对于播放列表文件的。

OK...now let's get to coding. Shown right is the opening of the source code from last month.

首先,我们需要为界面中定义的信号创建事件回调函数。需要注意的是on_MainWindow_destroyon_tbtnQuit_clicked这两个事件的回调函数我们已经搞定了。我们还需要创建下面这十几个事件毁掉函数。

We'll modify these stubbed routines in a few minutes. For now, this should get us up and running with an application, and we can test things as we go. But, we need to add one more line to the __init__ routine before we can run the app. After the self.wTree line, add...

self.SetEventDictionary() 

Now, you can run the application, see the window, and click the Quit toolbar button to exit the application properly. Save the code as "playlistmaker-1a.py" and give it a try. Remember to save it in the same folder as the glade file we created last time, or copy the glade file into the folder you saved this code in.

我们也需要定义一些变量以备使用。把这些定义加在__init__函数的SetEventDictionary后面即可。

self.CurrentPath = "" 
self.CurrentRow = 0 
self.RowCount = 0 

现在,我们需要一个弹出对话框,用于提示用户一些信息,并取得反馈。我们将使用内置类的一个函数gtk.MessageDialog实现这个功能。他的用法如下:

gtk.MessageDialog(parent,flags,MessageType,Buttons,message) 

在继续之前,我们需要先讨论一下消息类型。消息类型一般包含下面几种:

  • GTK_MESSAGE_INFO - 普通消息
  • GTK_MESSAGE_WARNING - 非紧急消息、警告
  • GTK_MESSAGE_QUESTION - 需要作出选择的问题
  • GTK_MESSAGE_ERROR - 紧急的错误消息

另外就是按钮的类型:

  • GTK_BUTTONS_NONE - 没有按钮
  • GTK_BUTTONS_OK - 包含一个确认(OK)按钮
  • GTK_BUTTONS_CLOSE - 包含一个退出(Close)按钮
  • GTK_BUTTONS_CANCEL - 一个取消(Cancel)按钮
  • GTK_BUTTONS_YES_NO - 是/否按钮组
  • GTK_BUTTONS_OK_CANCEL - 确认/取消按钮组

通常情况下,我们用下面的代码来创建并运行一个对话框,然后获取其相应消息,最后关闭之。

dlg = gtk.MessageDialog(None,0,gtk.MESSAGE_INFO,gtk.BUTTONS_OK,"This is a test message...") 
response = dlg.run() 
dlg.destroy() 

However, if you want to display a message box to the user more than once or twice, that's a LOT of typing. The general rule of thumb is that if you write a series of lines-of-code more than once or twice, it's usually better to create a function and then call that. Think of it this way: If we want to display a message dialog to the user, say ten times in your application, that's 10 X 3 (or 30) lines of code. By making a function to do this for us (using the example I just presented), we would have 10 + 3 (or 13) lines of code to write. The more we call a dialog, the less code we actually have to type, and the more readable our code is. Our function (top right) will allow us to call any of the four message dialog types with just one routine using different parameters.

This is a very simple function that we would then call like this...

self.MessageBox("info","The button QUIT was clicked") 

Notice that if we choose to use the MESSAGE_QUESTION type of dialog, there are two possible responses that will be returned by the message dialog - a "Yes" or a "No". Whichever button the user clicks, we will receive the information back in our code. To use the question dialog, the call would be something like this...

response = self.MessageBox("question","Are you sure you want to do this now?") 

if response == gtk.RESPONSE_YES: 

   print "Yes was clicked" 
elif response == gtk.RESPONSE_NO: 

   print "NO was clicked" 

You can see how you can check the value of the button returned. So now, replace the "pass" call in each of our event handler routines with something like that shown below right.

We won't keep it like this, but this gives you a visual indication that the buttons work the way we want. Save the code now as "playlistmaker-1b.py", and test your program. Now we are going to create a function to set our widget references. This routine is going to be called only once, but it will make our code much more manageable and readable. Basically, we want to create local variables that reference the widgets in our glade window - so we can make calls to them whenever (if ever) we need to. Put this function (above right) below the SetEventDictionary function.

Please notice that there is one thing that isn't referenced in our routine. That would be the treeview widget. We'll make that reference when we set up the treeview itself. Also of note is the last line of our routine. In order to use the status bar, we need to refer to it by its context id. We'll be using this later on.

Next, let's set up the function that displays the “about” dialog when we click the About toolbar button. Again, there is a built-in routine to do this provided by the GTK library. Put this after the MessageBox function. Here's the code, below right.

Save your code and then give it a try. You should see a pop-up box, centered in our application, that displays everything we have set. There are more attributes that you can set for the about box (which can be found at http://www.pygtk.org/docs/pygtk/class-gtkaboutdialog.html), but these are what I would consider a minimum set.

Before we go on, we need to discuss exactly what will happen from here. The general idea is that the user will click on the "Add" toolbar button, we'll pop up a file dialog box to allow them to add files to the playlist, and then display the file information into our treeview widget. From there, they can add more files, delete single file entries, delete all file entries, move a file entry up, down, or to the top or down to the bottom of the treeview. Eventually, they'll set the path that the file will be saved to, provide a filename with a "m3u" extension, and click the save file button. While this seems simple enough, there's a lot that happens behind the scenes. The magic all happens in the treeview widget, so let's discuss that. This will get pretty deep, so you might want to read carefully, since an understanding of this will keep you from making mistakes later on.

A treeview can be something as simple as a columnar list of data like a spreadsheet or database representation, or it could be more complex like a file-folder listing with parents and children, where the folder would be the parent and the files in that folder would be the children, or something even more complex. For this project, we'll use the first example, a columnar list. In the list, there will be three columns. One is for the name of the music file, one is for the extension of the file (mp3, ogg, wav, etc) and the final column is for the path. Combining this into a string (path, filename, extension) gives us the entry into the playlist we will be writing. You could, of course, add more columns as you wish, but for now, we'll deal with just three.

A treeview is simply a visual storage container that holds and displays a model. The model is the actual "device" that holds and manipulates our data. There are two different pre-defined models that are used with a treeview, but you can certainly create your own. That having been said, for 98% of your work, one of the two pre-defined models will do what you need. The two types are GTKListStore and GTKTreeStore. As their names suggest, the ListStore model is usually used for lists, the TreeStore is used for Trees. For our application, we will be using a GTKListStore. The basic steps are:

  • Create a reference to the TreeView widget.
  • Add the columns.
  • Set the type of renderer to use.
  • Create the ListStore.
  • Set the model attribute in the Treeview to our model.
  • Fill in the data.

The third step is to set up the type of renderer the column will use to display the data. This is simply a routine that is used to draw the data into the tree model. There are many different cell renderers that come with GTK, but most of the ones that you would normally use include GtkCellRenderText and GtkCellRendererToggle.

So, let's create a function (shown above) that sets up our TreeView widget. We'll call it SetupupTreeview. First we'll define some variables for our columns, set the variable reference of the TreeView itself, add the columns, set up the ListStore, and set the model. Here's the code for the function. Put it after the SetWidgetReferences function.

The variables cFName, cFType and cFPath define the column numbers. The variables sFName, sFType and sFPath will hold the column names in our displayed view. The seventh line sets the variable reference of the treeview widget as named in our glade file.

Next we call a routine (next page, top right), which we'll create in just a moment, for each column we want. Then we define our GTKListStore with three text fields, and finally set the model attribute of our TreeView widget to our GTKListStore. Let's create the AddPlaylistColumn function next. Put it after the SetupTreeview function.

Each column is created with this function. We pass in the title of the column (what's displayed on the top line of each column) and a columnID. In this case, the variables we set up earlier (sFName and cFname) will be passed here. We then create a column in our TreeView widget giving the title, what kind of cell renderer it will be using, and, finally, the id of the column. We then set the column to be resizable, set the sort id, and finally append the column into the TreeView.

Add these two functions to your code. I choose to put them right after the SetWidgetReferences function, but you can put it anywhere within the PlayListCreator class. Add the following line after the call to SetWidgetReferences() in the __init__ function to call the function.

self.SetupTreeview() 

Save and run your program, and you will see that we now have three columns with headers in our TreeView widget.

There are so many things left to do. We have to have a way to get the music filenames from the user and put them into the TreeView as rows of data. We have to create our Delete, ClearAll, movement functions, save routine, and file path routines, plus a few "pretty" things that will make our application look more professional. Let's start with the Add routine. After all, that's the first button on our toolbar. When the user clicks the Add button, we want to pop up a "standard" open-file dialog that allows for multiple selections. Once the user has made their selection, we then want to take this data and add it into the treeview, as I stated above. So the first logical thing to do is work on the File Dialog. Again, GTK provides us a way to call a "standard" file dialog in code. We could hard code this as just lines in the on_tbtnAdd_clicked event handler, but let's make a separate class to handle this. While we are at it, we can make this class handle not only a file OPEN dialog, but a folder SELECT dialog as well. As before with the MessageBox function, you can pull this into a snippet file that has all kinds of reusable routines for later use.

We'll start by defining a new class called FileDialog which will have only one function called ShowDialog. That function will take two parameters, one called 'which' (a '0' or a '1'), that designates whether we are creating an open-file or select-folder dialog, and the other is the path that should be used for the default view of the dialog called CurrentPath. Create this class just before our main code at the bottom of the source file.

class FileDialog: 

  def ShowDialog(self,which,CurrentPath): 

The first part of our code should be an IF statement

if which == 0: # file chooser 
   ... 
else:    # folder chooser 
   ... 

Before going any further, let's explore how the file/folder dialog is actually called and used. The syntax of the dialog is as follows

gtk.FileChooserDialog(title,parent,action,buttons,backend)

and returns a dialog object. Our first line (under if which == 0) will be the line shown below.

As you can see, the title is "Select files to add...", the parent is set to None. We are requesting a File Open type dialog (action), and we want a Cancel and an Open button, both using "stock" type icons. We are also setting the return codes of gtk.RESPONSE_CANCEL and gtk.RESPONSE_OK for when the user makes their selections. The call for our Folder Chooser under the Else clause is similar.

Basically, the only thing that changed between the two definitions are the title (shown above right) and the action type. So our code for the class should now be the code shown middle right.

These set the default response to be the OK button, and then to turn on the multiple select feature so the user can select (you guessed it) multiple files to add. If we didn't set this, the dialog would only allow one file to be selected at a time, since set_select_multiple is set to False by default. Our next lines are setting the current path, and then displaying the dialog itself. Before we type in the code, let me explain why we want to deal with the current path. Every time you pop up a file dialog box, and you DON'T set a path, the default is to the folder where our application resides. So, let's say that the music files that the user would be looking for are in /media/music_files/, and are then broken down by genre, and further by artist, and further by album. Let's further assume that the user has installed our application in /home/user2/playlistmaker. Each time we pop up the dialog, the starting folder would be /home/user2/playlistmaker. Quickly, the user would become frustrated by this, wanting the last folder he was in to be the starting folder next time. Make sense? OK. So, bottom right are our next lines of code.

Here we check the responses sent back. If the user clicked the 'Open' button which sends back a gtk.RESPONSE_OK, we get the name or names of the files the user selected, set the current path to the folder we are in, destroy the dialog, and then return the data back to the calling routine. If, on the other hand, the user clicked on the 'Cancel' button, we simply destroy the dialog. I put the print statement in there just to show you that the button press worked. You can leave it or take it out. Notice that when we return from the Open button part of the routine, we are returning two sets of values. 'fileselection' is a list of the files selected by the user, as well as the CurrentPath.

In order to get the routine to do something, add the following line under the on_tbtnAdd_click routine...

fd = FileDialog() 

selectedfiles,self.CurrentPath = fd.ShowDialog(0,self.CurrentPath) 

Here we retrieve the two return values that are sent from our return call. For now, add the following code to see what the information returned will look like.

for f in selectedfiles: 

   print "User selected %s" % f 

print "Current path is %s" % self.CurrentPath 

When you run the program, click on the 'Add' button. You'll see the file dialog. Now move to somewhere where you have some files and select them. You can hold down the [ctrl] key and click on multiple files to select them individually, or the [shift] key to select multiple contiguous files. Click on the 'Open' button, and look at the response in your terminal window. Please note that if you click on the 'Cancel' button right now, you'll get an error message. That's because the above code assumes that there are no files selected. Don't worry about that right now - we'll handle that in a little bit. I just wanted to let you see what comes back if the 'Open' button is pressed. One thing we should do is add a filter to our file-open dialog. Since we expect the user to normally select music files, we should (1) give the option to display only music files, and (2) give the option to show all files just-in-case. We do this by using the filefilter attributes of the dialog. Here's the code for that which should go in the which == 0 section right after the dialog set line.

filter = gtk.FileFilter() 
filter.set_name("Music Files") 
filter.add_pattern("*.mp3") 
filter.add_pattern("*.ogg") 
filter.add_pattern("*.wav") 
dialog.add_filter(filter) 
filter = gtk.FileFilter() 
filter.set_name("All files") 
filter.add_pattern("*") 
dialog.add_filter(filter)  

We are setting up two "groups", one for music files (filter.set_name("Music Files")), and the other for all files. We use a pattern to define the types of files we want. I have defined three patterns, but you can add or delete any that you wish. I put the music filter first, since that's what we will assume the user is going to be mainly concerned with. So the steps are...

• Define a filter variable. • Set the name. • Add a pattern. • Add the filter to the dialog.

You can have as many or as few filters as you wish. Also notice that once you have added the filter to the dialog, you can re-use the variable for the filter.

Back in the on_tbtnAdd_clicked routine, comment out the last lines we added and replace them with this one line.

self.AddFilesToTreeview(selectedfiles)

so our routine now looks like the code shown on the next page.

So, when we get the response back from file dialog, we will send the list containing the selected files to this routine. Once here, we set up a counter variable (how many files we are adding), then parse the list. Remember that each entry contains the fully qualified filename with path and extension. We'll want to split the filename into path, filename, and extension. First we get the very last 'period' from the filename and assume that is the beginning of the extension and assign its position in the string to extStart. Next we find the very last '/' in the filename to determine the beginning of the filename. Then we break up the string into extension, filename and file path. We then stuff these values into a list named 'data' and append this into our playlist ListStore. We increment the counter since we have done all the work. Finally we increment the variable RowCount which holds the total number of rows in our ListStore, and then we print a message to the status bar.

现在你可以运行一下我们的程序,在TreeView视图中看看我们的数据了。

这一讲的代码一如既往的可以在网上找到,地址是 http://pastebin.com/JtrhuE71.

在下一讲中,我们将完成我们的程序,将剩下的功能全部实现。

个人工具