這篇的篇名下得有點奇怪,我也不知如何命名之,主要是想要表達整合這幾個技術:
- 用python語言
- 使用gtk+來做出圖形界面
- 利用glade工具來編排圖形界面的佈局,而不是用程式來寫
- 結合dbus來控制圖形界面
之前有看到python dbus的範例程式:參見
Python DBus教學精要[1]的
example-service.py其程式主要架構為:
class CLASSNAME(dbus.service.Object)
@dbus.service.method("INTERFACE NAME")
def METHOD_NAME(self):
# do something here
if __name__ == '__main__':
# request dbus public name, instantiate object for dbus
# gobject mainloop run
注意到我們必須建立GObject的事件處理迴圈,因為這是在寫DBus服務,而服務是不會跑完馬上停止的,而是必須一直在等待是否有客戶端的要求進來。
另外也看到可以用python配合glade來寫gtk+的圖形界面: 參見
Python Gtk glade開發GUI程式[2]。其中python的程式為:
from gi.repository import Gtk
class Handler:
def onDeleteWindow(self, *args):
Gtk.main_quit(*args)
def onButtonPressed(self, button):
print "Hello World!"
builder = Gtk.Builder()
builder.add_from_file("builder_example.glade")
builder.connect_signals(Handler())
window = builder.get_object("window1")
window.show_all()
Gtk.main()
同樣,圖形界面也是程式跑起來後,必須一直等待使用者的輸入事件發生。所以其實和服務的程式寫法一樣,都必須有個事件迴圈。注意這裡的事件迴圈使用的是Gtk.main(),這下讓我想將兩者合在一起時,真不知道要使用那一個才是,或者要兩者一起用,或是要用其他的方式做呢?
這個問題的答案在這一篇文
how-export-methods-with-dbus-in-a-extended-class-in-python-inherited-methods[3]裡找到了一個可行的答案。其實此文不是在討論這個問題,只是他的範例程式也正好給了我們答案。他使用了Gtk.main()。但是也下了DBusGMainLoop()。
我將他的程式稍加修改:
#! /usr/bin/env python
# interface imports
from gi.repository import Gtk
# dbus imports
import dbus
import dbus.service
from dbus.mainloop.glib import DBusGMainLoop
# Main window class
class Window(dbus.service.Object):
def onDeleteWindow(self, *args):
Gtk.main_quit(*args)
def onButtonPressed(self, button):
label3 = self.builder.get_object("label3")
label3.set_text("o............o")
print "Hello World!"
def on_dialog_pressed(self, button):
self.show_dial()
def __init__(self, gladeFilePath, name):
self.builder = Gtk.Builder()
self.builder.add_from_file(gladeFilePath)
self.builder.connect_signals(self)
self.window = self.builder.get_object("window1")
self.dialog = self.builder.get_object("dialog1")
self.name = name
self.busName = dbus.service.BusName('com.example.MyTest', bus=dbus.SessionBus())
dbus.service.Object.__init__(self, self.busName, '/com/example/MyInterface/' + self.name)
@dbus.service.method('com.example.MyInterface.Window')
def show(self):
self.window.show_all()
@dbus.service.method('com.example.MyInterface.Window')
def show_dial(self):
label2 = self.builder.get_object("label2")
label2.set_text("ok!.............")
self.dialog.show()
response = self.dialog.run()
print response
#do stuff
self.dialog.hide()
@dbus.service.method('com.example.MyInterface.Window')
def hide(self):
self.window.hide()
@dbus.service.method('com.example.MyInterface.Window')
def destroy(self):
Gtk.main_quit()
@dbus.service.method('com.example.MyInterface.Window')
def update(self, data):
print(update)
# Child window class
class WindowOne(Window):
def __init__(self, gladeFilePath):
Window.__init__(self, gladeFilePath, "WindowOne")
@dbus.service.method('com.example.MyInterface.WindowOne')
def update(self, data):
# reimplementation of top class 'update' method
print "One update"
if __name__ == "__main__":
DBusGMainLoop(set_as_default=True)
gladeFilePath = "builder_example2.glade"
windowOne = WindowOne(gladeFilePath)
Gtk.main()
Python程式的好處之一就是程式區塊不使用{}或者begin,end關鍵字來表示,而是使用縮格來表示,如此一來程式會短一點,也比較容易閱讀。但是我在改這支程式時也被縮格給玩了好久。可能是我程式由網頁直接複製貼上編輯器的關係,也許有一些看不到的HTML碼也被貼上了,所以程式一跑,就一直跟我抱怨縮格沒對到。以下是已測過可正常執行的程式碼:
其中的
builder_exampler2.glade是使用glade工具做出來的,如Fig.1, Fig.2所示,我們拉了一個視窗(window)和一個對話框(dialog)。
|
Fig.1 |
|
Fig.2 |
$ ./glade-dbus.py
執行程式後,什麼東西都不會出現!
此時請先打開d-feet,切到Session Bus,為了快一點找到我們寫的服務,在篩選器輸入『com.example』就會看到我們的服務,如Fig.3所示。
|
Fig.3 |
請找到show()方法,直接按兩下後,會出現一個視窗,直接按『執行』按鍵就會看到出現了一個視窗,沒錯,這就是我們用glade設計的那一個視窗;如果要隱藏視窗,同樣可以在d-feet找到hide()方法並執行之就可以了。這表示,我們寫的程式既是一個圖形界面的程式,也是一個DBus的服務,而且使用者可以透過叫用DBus的方法(包括使用d-feet, dbus-send指令等)來操作圖形界面。
|
Fig.4 |
例如,當Fig.4的視窗出現時,我可以按一下其中的「dialog」按鍵來叫出一個對話框(Fig.5),我也可以在d-feet中找到show_dial()方法,執行它來叫出這個對話框。或者也可以透過執行以下dbus-send指令來做到同樣的動作。
$ dbus-send --print-reply --dest=com.example.MyTest /com/example/MyInterface/WindowOne com.example.MyInterface.Window.show_dial
method return sender=:1.233 -> dest=:1.240 reply_serial=2
|
Fig.5 |
最後提到一個技巧,我想要一個比較有彈性的對話框,每次叫出它之前可以改變它的提示文字,就像Fig.5所示,它的藍色文字其實是程式動態填上的。使用的程式寫法就是利用builder.get_object()取得對其Label的參考,再由此參考的set_text()方法來填入我們想要的文字。
檢視改寫的程式後,發現一個Class要做太多事(dbus, gtk+),所以再修改為以下版本:
#! /usr/bin/env python
# interface imports
from gi.repository import Gtk
# dbus imports
import dbus
import dbus.service
from dbus.mainloop.glib import DBusGMainLoop
# dbus class
class DBus(dbus.service.Object):
def __init__(self, window, name):
self.window = window
self.name = name
self.busName = dbus.service.BusName('com.example.MyTest', bus=dbus.SessionBus())
dbus.service.Object.__init__(self, self.busName, '/com/example/MyInterface/' + self.name)
@dbus.service.method('com.example.MyTest')
def show(self):
self.window.show_all()
@dbus.service.method('com.example.MyTest')
def hide(self):
self.window.hide()
# Main window class
class Window:
def onDeleteWindow(self, *args):
Gtk.main_quit(*args)
def onButtonPressed(self, button):
label3 = self.builder.get_object("label3")
label3.set_text("o............o")
print "Hello World!"
def on_dialog_pressed(self, button):
label2 = self.builder.get_object("label2")
label2.set_text("ok!.............")
self.dialog.show()
response = self.dialog.run()
print response
#do stuff
self.dialog.hide()
def __init__(self, gladeFilePath, name):
self.builder = Gtk.Builder()
self.builder.add_from_file(gladeFilePath)
self.builder.connect_signals(self)
self.window = self.builder.get_object("window1")
self.dialog = self.builder.get_object("dialog1")
if __name__ == "__main__":
DBusGMainLoop(set_as_default=True)
gladeFilePath = "builder_example2.glade"
windowOne = Window(gladeFilePath, "Test")
dbus = DBus(windowOne.window, "Test")
Gtk.main()
這個版本把原本的單一Class分為兩個Class,一個專司dbus的工作,另一個專司gtk+的工作。負責dbus的Class在需要用到GUI操作時,再去叫用另一個專司gtk+的Class來工作就可以了。這樣程式看來比原來的又簡潔了一些。
有一個特別的應用,可以把這個技術用上,就是配對藍牙設備。Bluez的source code解開後,在頂層目錄有一個/test的目錄,裡面有一支叫做simple-agent的python程式,大家都知道如果要在命令列配對藍牙設備,其中一個方法就是去用這支程式。使用方法:
$ simple-agent hci0 00:07:61:xx:xx:xx
RequestPinCode (/org/bluez/2345/hci0/dev_00_07_61_xx_xx_xx)
Enter PIN Code: 1234
Release
New device (/org/bluez/2345/hci0/dev_00_07_61_xx_xx_xx)
Ok, 這是命令列指令,如果我們想將其改寫成GUI界面的程式呢?
先研究一下simple-agent吧!我們發現其實它有一個Class叫做Agent,和這裡的範例程式的Class DBus或者原先的Class Window很像,都是會公開至DBus。如Fig.6所示。只是在我們的範例中,我們透過程式公開出來的方法,讓使用者可以由dbus來操作GUI,但是在simple-agent裡,其Class Agent是用來給bluez叫用的。所以我們可以簡單的改寫原來的範例程式,就可以達到目的:
|
Fig.6 |
#! /usr/bin/env python
# pair.py
# interface imports
from gi.repository import Gtk
# dbus imports
import dbus
import dbus.service
from dbus.mainloop.glib import DBusGMainLoop
class Rejected(dbus.DBusException):
_dbus_error_name = "org.bluez.Error.Rejected"
class Agent(dbus.service.Object):
exit_on_release = True
def __init__(self, window, bus, path):
self.window = window
self.path = path
self.bus = bus
dbus.service.Object.__init__(self, self.bus, self.path)
def set_exit_on_release(self, exit_on_release):
self.exit_on_release = exit_on_release
@dbus.service.method("org.bluez.Agent",
in_signature="", out_signature="")
def Release(self):
print "Release"
if self.exit_on_release:
Gtk.main_quit()
@dbus.service.method("org.bluez.Agent",
in_signature="os", out_signature="")
def Authorize(self, device, uuid):
print "Authorize (%s, %s)" % (device, uuid)
authorize = raw_input("Authorize connection (yes/no): ")
if (authorize == "yes"):
return
raise Rejected("Connection rejected by user")
@dbus.service.method("org.bluez.Agent",
in_signature="o", out_signature="s")
def RequestPinCode(self, device):
print "RequestPinCode (%s)" % (device)
return raw_input("Enter PIN Code: ")
@dbus.service.method("org.bluez.Agent",
in_signature="o", out_signature="u")
def RequestPasskey(self, device):
print "RequestPasskey (%s)" % (device)
passkey = raw_input("Enter passkey: ")
return dbus.UInt32(passkey)
@dbus.service.method("org.bluez.Agent",
in_signature="ou", out_signature="")
def DisplayPasskey(self, device, passkey):
print "DisplayPasskey (%s, %d)" % (device, passkey)
@dbus.service.method("org.bluez.Agent",
in_signature="ou", out_signature="")
def RequestConfirmation(self, device, passkey):
prompt_text = "RequestConfirmation (%s, %d)" % (device, passkey)
confirm = self.window.dialog_result(prompt_text)
if (confirm == "yes"):
return
raise Rejected("Passkey doesn't match")
@dbus.service.method("org.bluez.Agent",
in_signature="s", out_signature="")
def ConfirmModeChange(self, mode):
print "ConfirmModeChange (%s)" % (mode)
authorize = raw_input("Authorize mode change (yes/no): ")
if (authorize == "yes"):
return
raise Rejected("Mode change by user")
@dbus.service.method("org.bluez.Agent",
in_signature="", out_signature="")
def Cancel(self):
print "Cancel"
# Main window class
class Window:
def onDeleteWindow(self, *args):
Gtk.main_quit(*args)
def onButtonPressed(self, button):
my_entry = self.builder.get_object("entry1")
t1 = my_entry.get_text()
self.adapter.CreatePairedDevice(t1, self.path, self.capability,
reply_handler=create_device_reply,
error_handler=create_device_error)
def dialog_result(self, prompt_text):
#label1 = self.builder.get_object("label1")
#label1.set_text(device)
label2 = self.builder.get_object("label2")
label2.set_text(prompt_text)
self.dialog.show()
response = self.dialog.run()
#do stuff
self.dialog.hide()
if (response == 1):
return 'yes'
return 'no'
def __init__(self, gladeFilePath, name, adapter, path, capability):
self.builder = Gtk.Builder()
self.builder.add_from_file(gladeFilePath)
self.builder.connect_signals(self)
self.window = self.builder.get_object("window1")
self.dialog = self.builder.get_object("dialog1")
self.adapter = adapter
self.path = path
self.capability = capability
self.window.show_all()
def create_device_reply(device):
print "New device (%s)" % (device)
def create_device_error(error):
print "Creating device failed: %s" % (error)
if __name__ == "__main__":
DBusGMainLoop(set_as_default=True)
bus = dbus.SystemBus()
manager = dbus.Interface(bus.get_object("org.bluez", "/"), "org.bluez.Manager")
capability = "DisplayYesNo"
path = manager.DefaultAdapter()
adapter = dbus.Interface(bus.get_object("org.bluez", path), "org.bluez.Adapter")
path = "/test/agent"
gladeFilePath = "pair.glade"
windowOne = Window(gladeFilePath, "Test", adapter, path, capability)
agent = Agent(windowOne, bus, path)
agent.set_exit_on_release(False)
Gtk.main()
|
Fig.7 |
|
Fig.8 |
程式一執行,會出現Fig.8的視窗,在此我們可以輸入想要配對的藍牙設備的MAC位址,然後按Go Pair按紐。如果你的藍牙設備是比較新的,就會出現如Fig.7所示的對話框。告訴你現在要配對的裝置應該會顯示某個數字(passkey),如果裝置上的數字和此相同,請按Yes, 否則按No。用這個程序就可以完成藍牙設備的配對工作。因為我手上的設備是比較新的,所以我只改寫這個部份的程式,如果你手上是比較舊的設備,例如5,6年前的手機,那麼就不會是這個程序(passkey),而可能是要求輸入PIN碼,此部份程式我沒有修改,請自行依照我的修改方式進行程式改寫就可以了。
參考來源:
[1] 石頭成, Python DBus教學精要, 石頭閒語, 2011/4/14
[2] 小白和小叮叮, Python Gtk glade開發GUI程式, 2013/5/14
[3] Msum, How export methods with dbus in a extended class in python, inherited methods?, stackoverflow, 2012/12/7
留言