Python GUI 开发:从基础到应用
1. Python 进程处理与守护进程
在 Python 中处理进程时,我们能感受到它的成熟与强大。Python 有着优雅且复杂的线程 API,但全局解释器锁(GIL)是需要时刻牢记的一点。如果程序是 I/O 密集型的,GIL 通常不会造成问题;但如果需要使用多个处理器,那么使用进程是个不错的选择。甚至有人认为,即便没有 GIL,使用进程也比使用线程更好,主要原因在于调试线程代码可能会是一场噩梦。
下面是一个简单的守护进程示例,它能帮助我们理解守护进程的基本概念:
No mod 5 at 1207272456.18
No mod 5 at 1207272457.19
No mod 5 at 1207272458.19
No mod 5 at 1207272459.19
No mod 5 at 1207272461.2
No mod 5 at 1207272462.2
No mod 5 at 1207272463.2
No mod 5 at 1207272464.2
No mod 5 at 1207272466.2
No mod 5 at 1207272467.2
No mod 5 at 1207272468.2
No mod 5 at 1207272469.2
No mod 5 at 1207272471.2
No mod 5 at 1207272472.2
jmjones@dinkgutsy:code$ cat /tmp/stderr.log
Mod 5 at 1207272455.18
Mod 5 at 1207272460.2
Mod 5 at 1207272465.2
Mod 5 at 1207272470.2
这个简单的示例展示了守护进程的基本概念,我们可以利用守护进程编写目录监控器、网络监视器、网络服务器等长时间运行的程序。
此外,熟悉
Subprocess
模块也是个不错的选择,它是处理子进程的一站式解决方案。
2. GUI 开发基础理论
在系统管理工作中,构建 GUI 应用程序可能不是首先想到的任务。但在某些情况下,构建 GUI 应用程序能让工作变得更轻松。这里的 GUI 涵盖了传统的使用 GTK 和 QT 等工具包的应用程序,以及基于 Web 的应用程序。
当编写控制台实用程序时,通常期望它能在无需用户干预的情况下运行并完成任务。但编写 GUI 实用程序时,需要用户提供一些输入才能使程序正常工作。
GUI 应用程序的行为是事件驱动的。应用程序等待事件发生,例如按钮被按下或复选框被选中。程序员会将特定事件与特定代码块关联起来,这些代码块被称为事件处理程序。GUI 工具包的一项重要任务就是在关联事件发生时调用正确的事件处理程序,它通过提供一个“事件循环”来实现这一点,该循环会不断等待事件发生,并在事件发生时进行适当处理。
在创建 GUI 应用程序的界面时,有两种方式可供选择:
-
使用 GUI 构建器
:不同的 GUI 工具包可能有对应的 GUI 构建器,例如在 Mac 上编写 Cocoa 应用程序可以使用 Interface Builder,在 Linux 上使用 PyGTK 可以使用 Glade,使用 PyQT 可以使用 QT Designer。
-
手动编写代码
:如果需要对 GUI 有更多的控制,也可以通过编写代码来手动布局 GUI。在 PyGTK 中,每种 GUI 组件都对应一个 Python 类,例如窗口是
gtk.Window
类的对象,按钮是
gtk.Button
类的对象。
3. 构建简单的 PyGTK 应用程序
在运行示例或编写自己的 PyGTK 应用程序之前,需要先安装 PyGTK。在较新的 Linux 发行版上安装相对简单,在 Windows 上看起来也比较容易,如果使用的是 Ubuntu,它可能已经安装好了。如果没有适用于你平台的二进制发行版,安装过程可能会比较麻烦。
以下是一个简单的 PyGTK 应用程序示例,它包含一个窗口和一个按钮,点击按钮时会更新按钮的标签显示当前时间:
#!/usr/bin/env python
import pygtk
import gtk
import time
class SimpleButtonApp(object):
"""This is a simple PyGTK app that has one window and one button.
When the button is clicked, it updates the button's label with the current time.
"""
def __init__(self):
#the main window of the application
self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
#this is how you "register" an event handler. Basically, this
#tells the gtk main loop to call self.quit() when the window "emits"
#the "destroy" signal.
self.window.connect("destroy", self.quit)
#a button labeled "Click Me"
self.button = gtk.Button("Click Me")
#another registration of an event handler. This time, when the
#button "emits" the "clicked" signal, the 'update_button_label'
#method will get called.
self.button.connect("clicked", self.update_button_label, None)
#The window is a container. The "add" method puts the button
#inside the window.
self.window.add(self.button)
#This call makes the button visible, but it won't become visible
#until its container becomes visible as well.
self.button.show()
#Makes the container visible
self.window.show()
def update_button_label(self, widget, data=None):
"""set the button label to the current time
This is the handler method for the 'clicked' event of the button
"""
self.button.set_label(time.asctime())
def quit(self, widget, data=None):
"""stop the main gtk event loop
When you close the main window, it will go away, but if you don't
tell the gtk main event loop to stop running, the application will
continue to run even though it will look like nothing is really
happening.
"""
gtk.main_quit()
def main(self):
"""start the gtk main event loop"""
gtk.main()
if __name__ == "__main__":
s = SimpleButtonApp()
s.main()
在这个示例中,主类继承自
object
而不是某个 GTK 类。创建 PyGTK GUI 应用程序不一定需要采用面向对象的方式,但对于更复杂的应用,创建自定义类是个很好的选择,这样可以方便地管理和访问所有 GUI 组件。
4. 使用 PyGTK 构建 Apache 日志查看器
接下来,我们将使用 PyGTK 构建一个更实用的应用程序——Apache 日志查看器,它具备以下功能:
- 选择并打开指定的日志文件
- 一眼查看行号、远程主机、状态和发送的字节数
- 按行号、远程主机、状态或发送的字节数对日志行进行排序
以下是该日志查看器的源代码:
#!/usr/bin/env python
import gtk
from apache_log_parser_regex import dictify_logline
class ApacheLogViewer(object):
"""Apache log file viewer which sorts on various pieces of data"""
def __init__(self):
#the main window of the application
self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
self.window.set_size_request(640, 480)
self.window.maximize()
#stop event loop on window destroy
self.window.connect("destroy", self.quit)
#a VBox is a container that holds other GUI objects primarily for layout
self.outer_vbox = gtk.VBox()
#toolbar which contains the open and quit buttons
self.toolbar = gtk.Toolbar()
#create open and quit buttons and icons
#add buttons to toolbar
#associate buttons with correct handlers
open_icon = gtk.Image()
quit_icon = gtk.Image()
open_icon.set_from_stock(gtk.STOCK_OPEN, gtk.ICON_SIZE_LARGE_TOOLBAR)
quit_icon.set_from_stock(gtk.STOCK_QUIT, gtk.ICON_SIZE_LARGE_TOOLBAR)
self.open_button = gtk.ToolButton(icon_widget=open_icon)
self.quit_button = gtk.ToolButton(icon_widget=quit_icon)
self.open_button.connect("clicked", self.show_file_chooser)
self.quit_button.connect("clicked", self.quit)
self.toolbar.insert(self.open_button, 0)
self.toolbar.insert(self.quit_button, 1)
#a control to select which file to open
self.file_chooser = gtk.FileChooserWidget()
self.file_chooser.connect("file_activated", self.load_logfile)
#a ListStore holds data that is tied to a list view
#this ListStore will store tabular data of the form:
#line_numer, remote_host, status, bytes_sent, logline
self.loglines_store = gtk.ListStore(int, str, str, int, str)
#associate the tree with the data...
self.loglines_tree = gtk.TreeView(model=self.loglines_store)
#...and set up the proper columns for it
self.add_column(self.loglines_tree, 'Line Number', 0)
self.add_column(self.loglines_tree, 'Remote Host', 1)
self.add_column(self.loglines_tree, 'Status', 2)
self.add_column(self.loglines_tree, 'Bytes Sent', 3)
self.add_column(self.loglines_tree, 'Logline', 4)
#make the area that holds the apache log scrollable
self.loglines_window = gtk.ScrolledWindow()
#pack things together
self.window.add(self.outer_vbox)
self.outer_vbox.pack_start(self.toolbar, False, False)
self.outer_vbox.pack_start(self.file_chooser)
self.outer_vbox.pack_start(self.loglines_window)
self.loglines_window.add(self.loglines_tree)
#make everything visible
self.window.show_all()
#but specifically hide the file chooser
self.file_chooser.hide()
def add_column(self, tree_view, title, columnId, sortable=True):
column = gtk.TreeViewColumn(title, gtk.CellRendererText() , text=columnId)
column.set_resizable(True)
column.set_sort_column_id(columnId)
tree_view.append_column(column)
def show_file_chooser(self, widget, data=None):
"""make the file chooser dialog visible"""
self.file_chooser.show()
def load_logfile(self, widget, data=None):
"""load logfile data into tree view"""
filename = widget.get_filename()
print "FILE-->", filename
self.file_chooser.hide()
self.loglines_store.clear()
logfile = open(filename, 'r')
for i, line in enumerate(logfile):
line_dict = dictify_logline(line)
self.loglines_store.append([i + 1, line_dict['remote_host'],
line_dict['status'], int(line_dict['bytes_sent']), line])
logfile.close()
def quit(self, widget, data=None):
"""stop the main gtk event loop"""
gtk.main_quit()
def main(self):
"""start the gtk main event loop"""
gtk.main()
if __name__ == "__main__":
l = ApacheLogViewer()
l.main()
在这个示例中,
ApacheLogViewer
类继承自
object
,主要的初始化工作在
__init__
方法中完成。我们创建了一个窗口对象,并设置了其大小和最大化属性。使用
VBox
作为容器来布局其他 GUI 组件,添加了工具栏和工具按钮,创建了文件选择器和用于显示日志行的列表组件,最后将所有组件组合在一起并设置可见性。
以下是该应用程序的创建流程:
graph TD
A[创建窗口对象] --> B[设置窗口大小和最大化]
B --> C[创建 VBox 容器]
C --> D[创建工具栏和工具按钮]
D --> E[创建文件选择器]
E --> F[创建列表组件(ListStore 和 TreeView)]
F --> G[设置列表组件的列]
G --> H[创建滚动窗口]
H --> I[将组件添加到 VBox 并设置可见性]
当启动这个应用程序时,我们可以选择并打开指定的日志文件,在列表控件中查看日志的行号、远程主机、状态和发送的字节数,还可以通过点击列标题对日志行进行排序。
5. 使用 Curses 构建 Apache 日志查看器
curses
是一个用于创建交互式文本界面应用程序的库。与 GUI 工具包不同,
curses
不遵循事件处理和回调的方式,需要我们自己负责获取用户输入并进行相应处理。同时,在
curses
中通常是直接在屏幕上绘制文本,而不是像 GUI 工具包那样将小部件添加到容器中。
以下是使用 Python 标准库中的
curses
模块实现的 Apache 日志查看器示例:
#!/usr/bin/env python
"""
curses based Apache log viewer
Usage:
curses_log_viewer.py logfile
This will start an interactive, keyboard driven log viewing application. Here
are what the various key presses do:
u/d - scroll up/down
t - go to the top of the log file
q - quit
b/h/s - sort by bytes/hostname/status
r - restore to initial sort order
"""
import curses
from apache_log_parser_regex import dictify_logline
import sys
import operator
class CursesLogViewer(object):
def __init__(self, logfile=None):
self.screen = curses.initscr()
self.curr_topline = 0
self.logfile = logfile
self.loglines = []
def page_up(self):
self.curr_topline = self.curr_topline - (2 * curses.LINES)
if self.curr_topline < 0:
self.curr_topline = 0
self.draw_loglines()
def page_down(self):
self.draw_loglines()
def top(self):
self.curr_topline = 0
self.draw_loglines()
def sortby(self, field):
#self.loglines = sorted(self.loglines, key=operator.itemgetter(field))
self.loglines.sort(key=operator.itemgetter(field))
self.top()
def set_logfile(self, logfile):
self.logfile = logfile
self.load_loglines()
def load_loglines(self):
self.loglines = []
logfile = open(self.logfile, 'r')
for i, line in enumerate(logfile):
line_dict = dictify_logline(line)
self.loglines.append((i + 1, line_dict['remote_host'],
line_dict['status'], int(line_dict['bytes_sent']), line.rstrip()))
logfile.close()
self.draw_loglines()
def draw_loglines(self):
self.screen.clear()
status_col = 4
bytes_col = 6
remote_host_col = 16
status_start = 0
bytes_start = 4
remote_host_start = 10
line_start = 26
logline_cols = curses.COLS - status_col - bytes_col - remote_host_col - 1
for i in range(curses.LINES):
c = self.curr_topline
try:
curr_line = self.loglines[c]
except IndexError:
break
self.screen.addstr(i, status_start, str(curr_line[2]))
self.screen.addstr(i, bytes_start, str(curr_line[3]))
self.screen.addstr(i, remote_host_start, str(curr_line[1]))
#self.screen.addstr(i, line_start, str(curr_line[4])[logline_cols])
self.screen.addstr(i, line_start, str(curr_line[4]), logline_cols)
self.curr_topline += 1
self.screen.refresh()
def main_loop(self, stdscr):
stdscr.clear()
self.load_loglines()
while True:
c = self.screen.getch()
try:
c = chr(c)
except ValueError:
continue
if c == 'd':
self.page_down()
elif c == 'u':
self.page_up()
elif c == 't':
self.top()
elif c == 'b':
self.sortby(3)
elif c == 'h':
self.sortby(1)
elif c == 's':
self.sortby(2)
elif c == 'r':
self.sortby(0)
elif c == 'q':
break
if __name__ == '__main__':
infile = sys.argv[1]
c = CursesLogViewer(infile)
curses.wrapper(c.main_loop)
在这个示例中,我们创建了
CursesLogViewer
类来结构代码。在构造函数中,创建了
curses
屏幕并初始化了一些变量。在程序的主函数中,实例化
CursesLogViewer
并传入要查看的日志文件。然后将
main_loop
方法传递给
curses
的
wrapper
函数,该函数会设置终端状态以适合
curses
应用程序,调用传入的函数,最后恢复终端状态。
main_loop
方法充当一个简单的事件循环,等待用户的键盘输入。根据用户输入调用相应的方法,例如按下
u
或
d
键分别调用
page_up
或
page_down
方法来实现日志的上下滚动;按下
b
、
h
、
s
键分别按字节数、主机名、状态对日志进行排序;按下
r
键恢复到初始排序;按下
q
键退出程序。
以下是该应用程序的操作流程:
| 按键 | 操作 |
| ---- | ---- |
| u | 向上滚动日志 |
| d | 向下滚动日志 |
| t | 跳转到日志文件顶部 |
| q | 退出程序 |
| b | 按字节数排序 |
| h | 按主机名排序 |
| s | 按状态排序 |
| r | 恢复初始排序 |
graph TD
A[启动程序,传入日志文件] --> B[初始化 CursesLogViewer]
B --> C[调用 main_loop 方法]
C --> D[获取用户键盘输入]
D --> E{判断输入}
E -- u --> F[调用 page_up 方法]
E -- d --> G[调用 page_down 方法]
E -- t --> H[调用 top 方法]
E -- b --> I[调用 sortby 方法按字节数排序]
E -- h --> J[调用 sortby 方法按主机名排序]
E -- s --> K[调用 sortby 方法按状态排序]
E -- r --> L[调用 sortby 方法恢复初始排序]
E -- q --> M[退出循环,结束程序]
F --> D
G --> D
H --> D
I --> D
J --> D
K --> D
L --> D
虽然 PyGTK 应用程序和
curses
应用程序的代码行数相当,但
curses
应用程序可能感觉更复杂,可能是因为需要自己创建事件循环、自己创建“小部件”以及直接在终端屏幕上绘制文本。不过,在某些情况下,掌握
curses
应用程序的编写方法会很有帮助。例如,在一些资源受限或只能使用文本界面的环境中,
curses
可以发挥重要作用。
我们还可以对这个
curses
应用程序进行一些改进,例如添加反转当前排序顺序的功能,这是一个相对简单的修改,有兴趣的读者可以自行尝试。
超级会员免费看
2040

被折叠的 条评论
为什么被折叠?



