システムトレイ上のALSA用ミキサーを作成
- PyGTKでシステムトレイのアイコンの隣にポップアップウィンドウを作成する(前半)
- システムトレイアイコンの並び方向に関する覚え書き(2009/2/10現在)
- PyGTKでシステムトレイのアイコンの隣にポップアップウィンドウを作成する(後半)
- システムトレイアイコンの隣のポップアップウィンドウにスライダーを表示する(前半)
- システムトレイアイコンの隣のポップアップウィンドウにスライダーを表示する(後半)
- PythonからALSAのミキサーを扱う
あたりの内容を踏まえ、システムトレイに常駐するミキサー(ALSA向け)を作成した。
簡単に操作できるようにするため(と手抜きのため)、チャンネルごとに音量変更はできない。
スライダーが出ているときに外部アプリケーションがミキサー値を変更した場合に値を更新できるようにするため、スライダーのウィンドウが出ているときには指定した間隔ごとにミキサー値の取得とスライダーへの反映を行うことができるようにしている(閉じたらこの処理は必要なくなるのでタイマーを解除している)。
(2009/2/25)タイマーについては「PyGTK上のタイマー処理について」を参照。
そのミキサー値の更新やスライダーの長さ、ミキサー項目といった設定項目は${HOME}/.config/pytraymixer.confというファイルに保存するようにしている。
作成はしたがサンプル的な目的で実際には使っていないこともあり、作り込みの足らない部分も結構ある。
[任意]ファイル名: pytraymixer.py
#! /usr/bin/python # -*- encoding: utf-8 -*- import shlex import errno import sys import os try: import pygtk pygtk.require("2.0") except: pass try: import gobject import gtk except: print >> sys.stderr, "Error: PyGTK is not installed" sys.exit(1) if gtk.pygtk_version < (2,10,0): errtitle = "Error" errmsg = "PyGTK >= 2.10.0 required" if gtk.pygtk_version < (2,4,0): print >> sys.stderr, errtitle + ": " + errmsg else: errdlg = gtk.MessageDialog(type=gtk.MESSAGE_ERROR, buttons=gtk.BUTTONS_OK) errdlg.set_title(errtitle) errdlg.set_markup(errmsg) errdlg.run() sys.exit(1) try: import alsaaudio except: errtitle = "Error" errmsg = "pyalsaaudio required" errdlg = gtk.MessageDialog(type=gtk.MESSAGE_ERROR, buttons=gtk.BUTTONS_OK) errdlg.set_title(errtitle) errdlg.set_markup(errmsg) errdlg.run() sys.exit(1) # 設定ファイルに保存される項目 configdic = {"mixername" : None, "sliderlen" : None, "reflesh" : None, "interval" : None, } def run_errordialog(title, markup): """ エラーダイアログを表示 """ msgdlg_args = {"type" : gtk.MESSAGE_ERROR, "buttons" : gtk.BUTTONS_OK, } errdlg = gtk.MessageDialog(**msgdlg_args) errdlg.set_title(title) errdlg.set_markup(markup) errdlg.run() errdlg.destroy() class ConfigDialog(gtk.Dialog): """ 設定ダイアログ """ def __init__(self, *args, **kwargs): global configdic gtk.Dialog.__init__(self, *args, **kwargs) self.label_mixer = gtk.Label("Mixer:") self.combo_mixer = gtk.combo_box_new_text() for m in alsaaudio.mixers(): self.combo_mixer.append_text(m) self.combo_mixer.set_active(int(configdic["mixername"])) self.label_sliderlen = gtk.Label("Slider length:") self.label_interval1 = gtk.Label("every") self.label_interval2 = gtk.Label("milliseconds") self.chkbtn_reflesh = gtk.CheckButton("Reflesh volume:") self.chkbtn_reflesh.set_active(bool(int(configdic["reflesh"]))) self.spinbtn_sliderlen = gtk.SpinButton(gtk.Adjustment(float(configdic["sliderlen"]), 5, 20, 1, 0, 0)) self.spinbtn_sliderlen.set_numeric(True) self.spinbtn_interval = gtk.SpinButton(gtk.Adjustment(float(configdic["interval"]), 1, 99999, 1, 0, 0)) self.spinbtn_interval.set_numeric(True) self.hbox_mixer = gtk.HBox() self.hbox_mixer.pack_start(self.label_mixer, False, False, 0) self.hbox_mixer.pack_end(self.combo_mixer, False, False, 0) self.hbox_sliderlen = gtk.HBox() self.hbox_sliderlen.pack_start(self.label_sliderlen, False, False, 0) self.hbox_sliderlen.pack_end(self.spinbtn_sliderlen, False, False, 0) self.hbox_reflesh = gtk.HBox() self.hbox_reflesh.pack_start(self.chkbtn_reflesh, False, False, 0) self.hbox_reflesh.pack_start(self.label_interval1, False, False, 0) self.hbox_reflesh.pack_start(self.spinbtn_interval, False, False, 0) self.hbox_reflesh.pack_end(self.label_interval2, False, False, 0) self.vbox.add(self.hbox_mixer) self.vbox.add(self.hbox_sliderlen) self.vbox.add(self.hbox_reflesh) self.combo_mixer.connect("changed", self.combo_changed) self.spinbtn_sliderlen.connect("value-changed", self.spinbtn_changed, "sliderlen") self.spinbtn_interval.connect("value-changed", self.spinbtn_changed, "interval") self.chkbtn_reflesh.connect("toggled", self.chkbtn_toggled) self.connect("response", self.response) self.show_all() def response(self, widget, response_id): """ ダイアログを隠す """ self.hide() def combo_changed(self, widget): """ オーディオ出力先のコンボボックス項目の変更を反映 """ global configdic configdic["mixername"] = widget.get_active() def spinbtn_changed(self, widget, name): """ スピンボタン値の変更を反映 """ global configdic configdic[name] = widget.get_value_as_int() def chkbtn_toggled(self, widget): """ 音量表示更新のチェックボタンの変更を反映 """ global configdic configdic["reflesh"] = int(widget.get_active()) # True:1 False:0 class TrayMenu(gtk.Menu): """ システムトレイのメニュー """ def __init__(self, tray): gtk.Menu.__init__(self) # 必須 self.tray = tray self.show = False # ウィンドウの表示フラグ self.channel = alsaaudio.MIXER_CHANNEL_ALL # 音量変更するチャンネル self.timeout_id = None # 設定ダイアログ self.item_config = gtk.ImageMenuItem(stock_id=gtk.STOCK_PREFERENCES) self.item_config.connect("activate", self.config_activate) self.append(self.item_config) # 終了 self.item_quit = gtk.ImageMenuItem(stock_id=gtk.STOCK_QUIT) self.item_quit.connect("activate", self.quit_activate) self.append(self.item_quit) # 表示可能にする self.show_all() def activate(self, widget): """ クリックされたときの処理 """ global configdic # ウィンドウが表示されている場合は壊す if self.show == True: self.win.destroy() # ミキサー値更新のタイマーが設定されている場合は解除 if self.timeout_id: gobject.source_remove(self.timeout_id) self.timeout_id = None self.show = False # 表示されていない場合にウィンドウ作成 else: # (画面の情報, 表示領域の情報, 方向の情報) を取得 (screen, area, orientation) = self.tray.get_geometry() # ポップアップウィンドウの作成 self.win = gtk.Window(gtk.WINDOW_POPUP) # システムトレイアイコンが縦方向に並ぶ場合(パネルが左右端にある場合など) if orientation == gtk.ORIENTATION_VERTICAL: scale = gtk.HScale() # 画面右端にある場合は左に表示する # アイコンのX座標 + 幅 + (幅 x int(configdic["sliderlen"])) # つまり「アイコンのX座標 + 幅 x (1 + int(configdic["sliderlen"]))」を画面幅と比較 if area.x + area.width * (1 + int(configdic["sliderlen"])) > screen.get_width(): x = area.x - area.height * int(configdic["sliderlen"]) y = area.y width = area.width * int(configdic["sliderlen"]) height = area.height scale.set_value_pos(gtk.POS_RIGHT) # それ以外は右に表示する else: x = area.x + area.height y = area.y width = area.width * int(configdic["sliderlen"]) height = area.height scale.set_value_pos(gtk.POS_LEFT) # システムトレイアイコンが横方向に並ぶ場合(パネルが上下端にある場合など) else: scale = gtk.VScale() scale.set_inverted(True) # 上が大きい値になるようにする # 画面上端にある場合は下に表示する if area.y < area.height * int(configdic["sliderlen"]): x = area.x y = area.y + area.height width = area.width height = area.height * int(configdic["sliderlen"]) scale.set_value_pos(gtk.POS_TOP) # それ以外は上に表示する else: x = area.x y = area.y - area.height * int(configdic["sliderlen"]) width = area.width height = area.height * int(configdic["sliderlen"]) scale.set_value_pos(gtk.POS_BOTTOM) # 現在のミキサー値を取得 try: self.mixer = alsaaudio.Mixer(alsaaudio.mixers()[int(configdic["mixername"])]) except alsaaudio.ALSAAudioError: return # 現在の音量を取得 # 複数のチャンネルがある場合には平均する手もあるが # 今回は0番目の値をそのまま使用 volume = self.mixer.getvolume()[0] # スライダーの設定 # gtk.Range scale.set_update_policy(gtk.UPDATE_CONTINUOUS) # ドラッグ中も値を反映 scale.set_adjustment(gtk.Adjustment(value=volume, lower=0, upper=100, step_incr=1, page_incr=5, page_size=0)) # gtk.Scale scale.set_digits(0) # 小数点以下なし # シグナル(値変更時のハンドラと関連付け) scale.connect("value_changed", self.value_changed) # 上で決めたサイズと位置に調整 self.win.resize(width, height) self.win.move(x, y) # スライダーを追加 self.win.add(scale) # ウィンドウを表示 self.win.show_all() # ウィンドウ表示中のミキサー値の更新 if bool(int(configdic["reflesh"])) == True: self.timeout_id = gobject.timeout_add(int(configdic["interval"]), self.timeout, scale) self.show = True def config_activate(self, widget): """ 設定ダイアログを表示 """ self.confdlg = ConfigDialog(title="config", buttons=(gtk.STOCK_CLOSE, gtk.RESPONSE_CLOSE)) def value_changed(self, widget): """ スライダー値が変更されたときにミキサー値を変更 """ self.mixer.setvolume(int(widget.get_value()), self.channel) def timeout(self, scale): """ 音量が外部で変更されていないか、定期的にミキサー値を取得して更新 """ global configdic try: self.mixer = alsaaudio.Mixer(alsaaudio.mixers()[int(configdic["mixername"])]) except alsaaudio.ALSAAudioError: return False volume = self.mixer.getvolume()[0] scale.set_value(volume) return True # 繰り返す def show_menu(self, widget, button, time): """ メニューをポップアップ """ self.popup(None, None, gtk.status_icon_position_menu, button, time, self.tray) def quit_activate(self, widget): """ 設定を保存して終了 """ global rcfile, configdic # ディレクトリ~/.configが無ければ作成 rcdir = os.path.dirname(rcfile) try: os.mkdir(rcdir) except OSError, (errcode, strerr): # 既に存在する以外の理由で~/.configが作成できなければダイアログで表示 if errcode != errno.EEXIST: run_errordialog("Failed to make directory for settings", ('Cannot create directory "%(rcdir)s"' % {"rcdir" : rcdir}), strerr) gtk.main_quit() return # 設定ファイルに書き込む try: f_out = open(rcfile, "w") for (key, val) in configdic.iteritems(): f_out.write("%s\t%s\n" % (key, val)) f_out.close() except IOError, (errcode, strerr): run_errordialog("Failed to save config file", ('Cannot save config file "%(rcfile)s"' % {"rcfile" : rcfile}), strerr) self.confdlg.destroy() gtk.main_quit() class PyTrayMixer: """ システムトレイアイコンのミキサー """ def main(self): global rcfile, configdic rcfile = os.path.expanduser("~/.config/pytraymixer.conf") tray = gtk.StatusIcon() tray.set_from_stock(gtk.STOCK_DIALOG_INFO) # 仮のアイコン menu = TrayMenu(tray) tray.connect("popup-menu", menu.show_menu) tray.connect("activate", menu.activate) # 設定ファイルの読み込み try: f_in = open(rcfile, "r") lex = shlex.shlex(f_in) lex.whitespace_split = True while True: key = lex.get_token() val = lex.get_token() if not key or not val: break configdic[key] = val f_in.close() except IOError: # 各種設定の既定値を設定 configdic["mixername"] = 0 configdic["sliderlen"] = 8 configdic["reflesh"] = 0 configdic["interval"] = 500 gtk.main() if __name__ == "__main__": app = PyTrayMixer() app.main()
使用したバージョン:
- Python 2.5.2
- PyGTK 2.13.0
- pyalsaaudio 0.3