高見知英の技術ログ

技術関係のログをQiitaから移行してきました。プログラミングのほか、使っているアプリの細かい仕様についてなど書いていきます。

tkinterで表示するダイアログをマウスカーソル位置に表示する

Pythonで簡単なGUIを表示する場合、標準モジュールで何も追加しなくても使えるtkinterが便利です。

tkinterには、tkinter.simpledialog.Dialogというダイアログを表示するためのひな形クラスが存在するので、それを使用すると割と簡単にダイアログを作ることが可能です。

# 初期案
import tkinter.simpledialog

class ChooseDialog(tkinter.simpledialog.Dialog):

  def body(self, master) -> None:
    # 入力部分に表示するコントロールを作成する
    return super().body(master)

  def ok(self, event=None):  super().ok(event); self.result = True
  def cancel(self, event=None): super().cancel(event); self.result = False

owner = tkinter.Tk()
owner.withdraw()
dlg = ChooseDialog(owner, 'Choose Link Style')

ダイアログをマウスポインタの位置に表示したい

さて、このダイアログ、初期の設定だとウィンドウの中央に画面が表示されるようになっています*1

ただ、個人的にはちょっとしたダイアログなのでマウスカーソルの位置に表示させて、すぐに操作できるようにしたい。マウスポインタの位置を取得すればできるはずですので、やってみましょう。

# 動かないバージョン
import tkinter.simpledialog, ctypes, ctypes.wintypes

class ChooseDialog(tkinter.simpledialog.Dialog):

  def body(self, master) -> None:
    p = ctypes.wintypes.POINT()
    ctypes.windll.user32.GetCursorPos(ctypes.byref(p))
    
    self.geometry(f"+{p.x - self.winfo_width() // 2}+{p.y - self.winfo_height() // 2}")
    # 入力部分に表示するコントロールを作成する

    return super().body(master)

  def ok(self, event=None):  super().ok(event); self.result = True
  def cancel(self, event=None): super().cancel(event); self.result = False

Pythonからマウスカーソルを取得する方法については、ググってみても大抵「PyAutoGUI.positionを使え」と書いてあるのですが、マウスポインタをとるためだけにPyAutoGUIをインポートするのはちょっと・・・ と思い、PyAutoGUIのソースを参照してみたところ、ふつうにWindows APIGetCursorPosを呼んでるだけだったので、こちらを使いました。

さて、そんな感じでマウスカーソルの周囲にウィンドウを表示するよう、ウィンドウのジオメトリを設定するのですが、なぜか動きません。なぜか。

調べてみると、tkinter.simpledialog.Dialogはコンストラクタで_place_windowというプライベート関数を呼んでいるようで、このメソッドがダイアログの位置を設定しているようです。

それでも強引にウィンドウの位置を設定する

それでも自分でダイアログの表示位置を設定したい場合は、_place_window()関数の呼び出しよりあとにウィンドウの位置を矯正する必要がある。

と、いうことで、引数がなく副作用も最小限で済みそうな、grab_set()メソッドをオーバーライドして、処理を付け足します。

# 動くバージョン
import tkinter.simpledialog, ctypes, ctypes.wintypes

class ChooseDialog(tkinter.simpledialog.Dialog):

  def body(self, master) -> None:
    # 入力部分に表示するコントロールを作成する
    return super().body(master)

  def grab_set(self) -> None:
    p = ctypes.wintypes.POINT()
    ctypes.windll.user32.GetCursorPos(ctypes.byref(p))
    self.geometry(f"+{p.x - self.winfo_width() // 2}+{p.y - self.winfo_height() // 2}")
    return super().grab_set()

  def ok(self, event=None):  super().ok(event); self.result = True
  def cancel(self, event=None): super().cancel(event); self.result = False

こうすると、マウスカーソルの位置にウィンドウを表示させることができます。

わかったこと

  • tkinter.simpledialog.Dialogはちょっとしたダイアログを表示させたいときに結構便利
  • 挙動が分からないことがあったらソースコードを読もう(Visual Studio Codeからであれば、Ctrlキーを押しながらキーワードをクリックでもソースが表示できる)
  • Pythonの標準モジュールのソースはGithubでも公開されているのでそちらも確認を
  • なんだかんだ他人のソースコードを読みたいときに読めるようにしておくトレーニングは重要

参考文献

*1:の、はずなんですが、なぜか自分の環境では1モニタ目の画面左上に表示されます…。マルチスクリーンに対応していないのか・・・?