62916


現在、コントローラーのボタンを押した回数をカウントしてそれをGUIのアプリに表示させるスクリプトを書いているのですが、思ったような挙動になりません。

# やりたいこと

pythonのスクリプトを実行した時に下のようなGUIを表示させ、ボタンを押したと同時に該当する場所の数字のカウントが+1されるGUIを閉じたらプログラムが終了する。

[![画像の説明をここに入力](https://i.stack.imgur.com/0hmqy.png)](https://i.stack.imgur.com/0hmqy.png)

# 実際の挙動

スクリプトを実行しても、GUIは表示されない。Ctrl+Cを押したらGUIが表示され、閉じたらプログラムが終了する。

# スクリプト

[code]

    import pygame
    from pygame.locals import *
    import sys
    import time
    import tkinter
    from tkinter import font
    
    pygame.joystick.init()
    try:
       # ジョイスティックインスタンスの生成
       joystick = pygame.joystick.Joystick(0)
       joystick.init()
       print('ジョイスティックの名前:', joystick.get_name())
       print('ボタン数 :', joystick.get_numbuttons())
    except pygame.error:
       print('ジョイスティックが接続されていません')
    
    pygame.init()
    
    key_input = [0 for _ in range(9)]
    
    past_time = time.time()
    
    window = tkinter.Tk()
    window.title("Contoller input")
    window.geometry("640x360")
    
    canvas = tkinter.Canvas(window, width = 640, height = 360)
    
    canvas.create_rectangle(280, 195, 350, 330) #白鍵盤
    canvas.create_rectangle(370, 195, 440, 330)
    canvas.create_rectangle(460, 195, 530, 330)
    canvas.create_rectangle(550, 195, 620, 330)
    
    canvas.create_rectangle(325, 30, 395, 165, fill = "black") #黒鍵盤
    canvas.create_rectangle(415, 30, 485, 165, fill = "black")
    canvas.create_rectangle(505, 30, 575, 165, fill = "black")
    
    canvas.create_oval(35, 65, 255, 285) #皿
    canvas.create_line(145, 65, 145, 285)
    
    canvas.place(x=0, y=0)
    
    font1 = font.Font(family='Helvetica', size=10, weight='bold')
    
    label_1 = tkinter.Label(window, text=key_input[0], font=font1)
    label_2 = tkinter.Label(window, text=key_input[1], font=font1)
    label_3 = tkinter.Label(window, text=key_input[2], font=font1)
    label_4 = tkinter.Label(window, text=key_input[3], font=font1)
    label_5 = tkinter.Label(window, text=key_input[4], font=font1)
    label_6 = tkinter.Label(window, text=key_input[5], font=font1)
    label_7 = tkinter.Label(window, text=key_input[6], font=font1)
    label_8 = tkinter.Label(window, text=key_input[6], font=font1)
    label_9 = tkinter.Label(window, text=key_input[6], font=font1)
    
    label_1.place(x=310, y=333)
    label_2.place(x=355, y=5)
    label_3.place(x=400, y=333)
    label_4.place(x=445, y=5)
    label_5.place(x=490, y=333)
    label_6.place(x=535, y=5)
    label_7.place(x=580, y=333)
    label_8.place(x=205, y=300)
    label_9.place(x=100, y=300)
    
    
    while True:
        try:
            for e in pygame.event.get():
                if(e.type == pygame.locals.JOYBUTTONDOWN):
                    print("press button " + str(e.button))
                    key_input[e.button] += 1
                if(e.type == pygame.locals.JOYAXISMOTION): #LR2 Type
                    now_time = time.time()
                    delta_time = now_time - past_time
                    if(e.value < 0):
                        if(0.19 > delta_time or delta_time > 0.22):
                            print("Turntable spin to right")
                            key_input[7] += 1
                    if(e.value > 0):
                        key_input[8] += 1
                        print("Turntable spin to left")
                    past_time = now_time
        except KeyboardInterrupt:
            break
    
    window.mainloop()
    
[/code]

# 調べたこと

<https://shizenkarasuzon.hatenablog.com/entry/2018/12/31/064646>

などを参照しました。

よろしくお願いします。

# 追記

以下の通りソースコードを変更しました。(font1以降)

[code]

    for i in range(9):
        label.append(tkinter.Label(window, text=key_input[i], font=font1))
    
    label[0].place(x=310, y=333)
    label[1].place(x=355, y=5)
    label[2].place(x=400, y=333)
    label[3].place(x=445, y=5)
    label[4].place(x=490, y=333)
    label[5].place(x=535, y=5)
    label[6].place(x=580, y=333)
    label[7].place(x=205, y=300)
    label[8].place(x=100, y=300)
    
    def replace(num):
        if(num == 0):
            label[num].place(x=310, y=333)
        elif(num == 1):
            label[num].place(x=355, y=5)
        elif(num == 2):
            label[num].place(x=400, y=333)
        elif(num == 3):
            label[num].place(x=445, y=5)
        elif(num == 4):
            label[num].place(x=490, y=333)
        elif(num == 5):
            label[num].place(x=535, y=5)
        elif(num == 6):
            label[num].place(x=580, y=333)
        elif(num == 7):
            label[num].place(x=205, y=300)
        elif(num == 8):
            label[num].place(x=100, y=300)
    
    
    def pygame_loop():
        try:
            for e in pygame.event.get():
                if(e.type == pygame.locals.JOYBUTTONDOWN):
                    print("press button " + str(e.button))
                    key_input[e.button] += 1
                    label[e.button] = tkinter.Label(window, text=key_input[e.button], font=font1)
                    replace(e.button)
                if(e.type == pygame.locals.JOYAXISMOTION): #LR2 Type
                    now_time = time.time()
                    delta_time = now_time - past_time
                    if(e.value < 0):
                        if(0.19 > delta_time or delta_time > 0.22):
                            print("Turntable spin to right")
                            key_input[7] += 1
                            label[7] = tkinter.Label(window, text=key_input[7], font=font1)
                            replace(7)
                    if(e.value > 0):
                        key_input[8] += 1
                        print("Turntable spin to left")
                        label[8] = tkinter.Label(window, text=key_input[8], font=font1)
                        replace(8)
                    past_time = now_time
        except KeyboardInterrupt:
            pygame.quit()
        window.after(50, pygame_loop)
    
    pygame_loop()
    window.mainloop()
    
[/code]

ただ、画面は表示されますがボタンを押しても数字部分が更新されず、コンソールにも何も表示できていない状態です。


CC BY-SA 4.0
2020-02-08T13:44:14.550
62882
2020-02-10T15:03:17.427
2020-02-09T08:29:58.377
35838
35838
question
python tkinter
tkinterを用いて動的にカウントを更新したい


上手く動かない原因は、TkInterの主な処理が行われる`window.mainloop()`を呼ぶ前に、`while
True:`で永久ループしているからです。  
pygameのイベント処理で永久ループしているために、TkInterは呼ばれず処理が行われていません。

対処としては、pygameのイベント処理を1つの関数にして、永久ループではなく定期的に呼ばれるようにすれば良いと思われます。  
以下の記事はTkInterのみの内容ですが、参考になるでしょう。  
[ゼロからはじめるPython 第10回
100行程度でブロック崩しゲームを作ってみよう](https://news.mynavi.jp/article/zeropython-10/)

>
[code]

>     # ゲームループ --- (*5)
>     def game_loop():
>         draw_objects()
>         move_ball()
>         win.after(50, game_loop)
>  
>     game_loop()
>     win.mainloop() # ゲームウィンドウを表示
>  
[/code]

上記のような感じで、

[code]

    while True:
    
[/code]

の部分を

[code]

    def pygame_loop():
    
[/code]

に変え、

[code]

        except KeyboardInterrupt:
            break
    
    window.mainloop()
    
[/code]

の部分を

[code]

        except KeyboardInterrupt:
            pygame.quit()
    
        window.after(50, pygame_loop)
    
    pygame_loop()
    window.mainloop()
    
[/code]

に変えてみてはどうでしょう？

Joystickは持っていないので動作確認は出来ませんでしたが、画面表示される状態にはなりました。  
試してみてください。

他に参考になりそうな記事が以下にあります。読み解いてみてください。  
[Moving a Tkinter canvas object multiple steps using Pygame's joystick
class](https://stackoverflow.com/q/46505409/9014308)

[pygame - Pygameドキュメント 日本語訳](http://westplain.sakuraweb.com/translate/pygame/)

* * *

**追記**  
確かにイベントが通知されたり/されなかったりして安定していないようです。  
forループの先頭(以下の位置)に全ての受信イベントをprintする処理を入れて確認しました。

[code]

    for e in pygame.event.get():
        if(e.type == pygame.locals.JOYBUTTONDOWN):
    
[/code]

こんな感じです。

[code]

    for e in pygame.event.get():
        print('Notify Event ' + pygame.event.event_name(e.type))
        if(e.type == pygame.locals.JOYBUTTONDOWN):
    
[/code]

tkinterとpygameで入力を取り合っているのかもしれません。  
コメントに書いたpygameだけの記事では何時でも滑らかにイベント通知がされていました。

他に一応tkinterとpygameを組み合わせて使っている記事を見つけましたが、構成が変わるので参考になるかどうか。こんな記事です。  
[Tkinter How can I use Tkinter with pygame](https://python-forum.io/Thread-
Tkinter-How-can-I-use-Tkinter-with-pygame)

* * *

**さらに追記**  
どうやら、tkinterでpygameのイベントを受け取るためにはpygameのウインドウを作成してtkinterに埋め込む必要があるようです。  
ただし、記事自身は解決していないし、さらに紹介されている記事はwxPythonとか環境変数SDL_xxx設定とかを使った内容になっているので、そのまま応用できるか不明ですね。

[Print all pygame events in a tkinter
mainloop](https://stackoverflow.com/q/50487957/9014308)

> To receive pygame events, a pygame window / display **must** be initialized.
>
> "The input queue is heavily dependent on the pygame display module. If the
> display has not been initialized and a video mode not set, the event queue
> will not really work." [Pygame event
> documentation](https://www.pygame.org/docs/ref/event.html).
>
> However, if you do want to use tkinter with pygame, you can embed a pygame
> window into a tkinter window. This however will only give pygame events when
> using the embedded window. Here is an example from another
> [question](https://stackoverflow.com/questions/23319059/embedding-a-pygame-
> window-into-a-tkinter-or-wxpython-
> frame?utm_medium=organic&utm_source=google_rich_qa&utm_campaign=google_rich_qa):
>
> pygameイベントを受信するには、pygameウィンドウ/ディスプレイを初期化する **必要** があります。
>
>
> 「入力キューはpygameディスプレイモジュールに大きく依存しています。ディスプレイが初期化されておらず、ビデオモードが設定されていない場合、イベントキューは実際には機能しません。」
>
>
> ただし、pygameでtkinterを使用する場合は、pygameウィンドウをtkinterウィンドウに埋め込むことができます。ただし、これは埋め込みウィンドウを使用する場合にのみpygameイベントを提供します。

上記からさらに紹介されている関連記事など。  
[Embedding a Pygame window into a Tkinter or WxPython
frame](https://stackoverflow.com/q/23319059/9014308)  
[Tkinter embed Graphical
subprocess](https://stackoverflow.com/q/20483983/9014308)  
[How to get a form input using Tkinter in
Pygame?](https://stackoverflow.com/q/10777295/9014308)

ちなみにこちらの記事で、PyQt5をランチャー的に使ってpygameのウインドウを埋め込みではなく別途表示させるやり方で利用しています。  
[pygameのスクリーンをpyqt5のサブウィンドウから開いたらエラーが出た](https://ja.stackoverflow.com/q/54868/26370)

こうすれば出来るという記事。  
[Draw a circle in Pygame using
Tkinter](https://stackoverflow.com/q/13545911/9014308)  
[I'm embedding a pygame window into Tkinter, how do I manipulate the pygame
window?](https://stackoverflow.com/q/55755305/9014308)

逆に出来ない/しない方が良いという記事  
[Start/pause pygame via tkinter button in the same
window](https://stackoverflow.com/q/55086549/9014308)  
[pygame vs tkinter.](https://stackoverflow.com/q/10668116/9014308)  
[GUIs with pygame — wiki](https://www.pygame.org/wiki/gui)

* * *

**対策版**  
質問記事のソースをなるべく生かして対策版を作成してみました。  
Windows10 64bit, Python 3.7.6, pygame 1.9.6 です。

  * pygameウインドウを埋め込み動作させる
  * 鍵盤/皿はpygame側で描画する必要がある
  * Joystickの代替でキーボードの数字キーを使用
  * Unix系で'SDL_VIDEODRIVER'への設定は動作環境に依存するでしょう  
[pygame.display.init](http://westplain.sakuraweb.com/translate/pygame/Display.cgi#pygame.display.init)を参照

  * 画面が表示された後、いったん画面のどこかをマウスでクリックする必要がある

プログラムは以下になります。

[code]

    import pygame
    from pygame.locals import *
    import sys
    import time
    import tkinter
    from tkinter import font
    import os
    
    pygame.joystick.init()
    try:
       # ジョイスティックインスタンスの生成
       joystick = pygame.joystick.Joystick(0)
       joystick.init()
       print('ジョイスティックの名前:', joystick.get_name())
       print('ボタン数 :', joystick.get_numbuttons())
    except pygame.error:
       print('ジョイスティックが接続されていません')
    
    # pygame.init()は削除して後方のpygame.display.init()に変更
    key_input = [0 for _ in range(9)]
    past_time = time.time()
    
    window = tkinter.Tk()
    window.title("Contoller input")
    window.geometry("640x360")
    
    canvas = tkinter.Canvas(window, width = 640, height = 360)
    canvas.place(x=0, y=0)
    font1 = font.Font(family='Helvetica', size=10, weight='bold')
    
    label = []
    for i in range(9):
        label.append(tkinter.Label(window, text=key_input[i], font=font1))
    
    label[0].place(x=310, y=333)
    label[1].place(x=355, y=5)
    label[2].place(x=400, y=333)
    label[3].place(x=445, y=5)
    label[4].place(x=490, y=333)
    label[5].place(x=535, y=5)
    label[6].place(x=580, y=333)
    label[7].place(x=205, y=300)
    label[8].place(x=100, y=300)
    
    def replace(num):
        if(num == 0):
            label[num].place(x=310, y=333)
        elif(num == 1):
            label[num].place(x=355, y=5)
        elif(num == 2):
            label[num].place(x=400, y=333)
        elif(num == 3):
            label[num].place(x=445, y=5)
        elif(num == 4):
            label[num].place(x=490, y=333)
        elif(num == 5):
            label[num].place(x=535, y=5)
        elif(num == 6):
            label[num].place(x=580, y=333)
        elif(num == 7):
            label[num].place(x=205, y=300)
        elif(num == 8):
            label[num].place(x=100, y=300)
    
    #### 以後がpygame連携用処理
    os.environ['SDL_WINDOWID'] = str(canvas.winfo_id())
    # 以下は何か適切なものを選んで設定 pygame.display.initの説明を参照
    os.environ['SDL_VIDEODRIVER'] = 'windib'
    
    screen = pygame.display.set_mode((640,360))
    screen.fill(pygame.Color(240,240,240))
    
    pygame.display.init()
    
    # 鍵盤/皿をpygameで描画
    pgblack = pygame.Color(0, 0, 0)
    pygame.draw.rect(screen, pgblack, (280, 195, 70, 135), 1) #白鍵盤
    pygame.draw.rect(screen, pgblack, (370, 195, 70, 135), 1)
    pygame.draw.rect(screen, pgblack, (460, 195, 70, 135), 1)
    pygame.draw.rect(screen, pgblack, (550, 195, 70, 135), 1)
    
    pygame.draw.rect(screen, pgblack, (325, 30, 70, 135), 0) #黒鍵盤
    pygame.draw.rect(screen, pgblack, (415, 30, 70, 135), 0)
    pygame.draw.rect(screen, pgblack, (505, 30, 70, 135), 0)
    
    pygame.draw.circle(screen, pgblack, (145, 175), 110, 1) #皿
    pygame.draw.line(screen, pgblack, (145, 65), (145, 285), 1)
    
    pygame.display.update() # pygameの更新処理を呼んでおく
    window.update()         # tkinterの更新処理を呼んでおく
    
    while True:
        try:
            for e in pygame.event.get():
                print('Notify Event ' + pygame.event.event_name(e.type)) # 受信イベント表示
                if(e.type == pygame.locals.JOYBUTTONDOWN):
                    print("press button " + str(e.button))
                    key_input[e.button] += 1
                    label[e.button] = tkinter.Label(window, text=key_input[e.button], font=font1)
                    replace(e.button)
                if(e.type == pygame.locals.JOYAXISMOTION): #LR2 Type
                    now_time = time.time()
                    delta_time = now_time - past_time
                    if(e.value < 0):
                        if(0.19 > delta_time or delta_time > 0.22):
                            print("Turntable spin to right")
                            key_input[7] += 1
                            label[7] = tkinter.Label(window, text=key_input[7], font=font1)
                            replace(7)
                    if(e.value > 0):
                        key_input[8] += 1
                        print("Turntable spin to left")
                        label[8] = tkinter.Label(window, text=key_input[8], font=font1)
                        replace(8)
                    past_time = now_time
                #### 以下キーボードでの代替処理
                if e.type == QUIT:
                    pygame.quit()
                    sys.exit()
                    break
                if e.type == KEYDOWN:  # キーを押したとき
                    # ESCキーならスクリプトを終了
                    if e.key == K_ESCAPE:
                        pygame.quit()
                        sys.exit()
                        break
                    else:
                        print("押されたキー = " + pygame.key.name(e.key))
                        knum = -1
                        if e.key >= K_0 and e.key <= K_8:
                            knum = e.key - K_0
                        if e.key >= K_KP0 and e.key <= K_KP8:
                            knum = e.key - K_KP0
                        if knum >= 0:
                            key_input[knum] += 1
                            label[knum] = tkinter.Label(window, text=key_input[knum], font=font1)
                            replace(knum)
                #### ここまでキーボードでの代替処理
        except KeyboardInterrupt:
            pygame.quit()
            sys.exit()
            break
        pygame.display.update() # ループ中のpygameの更新処理
        window.update()         # ループ中のtkinterの更新処理
    
[/code]


CC BY-SA 4.0
2020-02-08T16:42:19.183
62886
2020-02-10T15:03:17.427
2020-06-17T08:14:45.997
-1
26370
62882
answer


Tkinter での処理部分を別スレッドにしてみました。ジョイスティックについては [Virtual
Gamepad](https://blog.marekkraus.sk/c/linuxs-uinput-usage-tutorial-virtual-
gamepad/) に記載されているソースコードを元にエミュレーションを行っています。  
最初にジョイスティックによる入力をエミュレートするコマンドを起動しておいてから以下のコードを実行します。その後、Tkinter
ウィンドウ内のカウンタの値が増加していく事を確認しています。

※ 実行環境は `python 3.7.5/pygame 1.9.6/Tkinter(Tcl/Tk) 8.6` です

[code]

    import os
    import sys
    import time
    import threading
    os.environ['PYGAME_HIDE_SUPPORT_PROMPT'] = 'hide'
    import pygame
    from pygame.locals import *
    import tkinter
    from tkinter import font, IntVar
    
    class GUI(threading.Thread):
      def __init__(self, title, key_input):
        self.title = title
        self.key_input = key_input
        self.is_running = False
        threading.Thread.__init__(self)
        self.setDaemon(True)
        self.start()
    
      def count_up(self, idx):
        if self.is_running:
          self.label_value[idx].set(self.label_value[idx].get()+1)
    
      def quit(self):
        if self.is_running:
          self.is_running = False
          self.window.quit()
    
      def run(self):
        self.window = tkinter.Tk()
        self.window.title(self.title)
        self.window.geometry('640x360')
    
        canvas = tkinter.Canvas(self.window, width = 640, height = 360)
        # 白鍵盤
        for i in range(4):
          canvas.create_rectangle(280+90*i, 195, 350+90*i, 330)
        # 黒鍵盤
        for i in range(3):
          canvas.create_rectangle(325+90*i, 30, 395+90*i, 165, fill = 'black')
        # 皿
        canvas.create_oval(35, 65, 255, 285) 
        canvas.create_line(145, 65, 145, 285)
    
        canvas.place(x=0, y=0)
    
        l = len(self.key_input)
        self.label = [None]*l
        self.label_value = [IntVar(value=v) for v in self.key_input]
        font1 = font.Font(family='Helvetica', size=10, weight='bold')
    
        for i in range(l):
          self.label[i] = tkinter.Label(
            self.window, font=font1, textvariable=self.label_value[i]
          )
          self.label[i].place(x=310+45*i, y=(5 if i < 7 and i%2 else 333))
        self.label[7].place(x=205, y=300)
        self.label[8].place(x=100, y=300)
    
        self.is_running = True
        self.window.mainloop()
    
    if __name__ == '__main__':
    
      pygame.init()
      try:
         joystick = pygame.joystick.Joystick(0)
         joystick.init()
      except pygame.error:
         print('ジョイスティックが接続されていません', file=sys.stderr)
         sys.exit(1)
    
      # Show joystick's specification
      print(f'ジョイスティックの名前: {joystick.get_name()}')
      print(f'ボタン数: {joystick.get_numbuttons()}')
    
      # Create Tkinter Window within another thread
      size = 9
      key_input = [0]*size
      g = GUI('Contoller input', key_input)
    
      # Event loop
      past_time = time.time()
      while True:
        try:
          for e in pygame.event.get():
            if(e.type == pygame.locals.JOYBUTTONDOWN):
              print(f'press button: {e.button}')
              if 0 <= e.button < size-2:
                g.count_up(e.button)
            if(e.type == pygame.locals.JOYAXISMOTION): #LR2 Type
              now_time = time.time()
              delta_time = now_time - past_time
              if(e.value < 0):
                if(0.19 > delta_time or delta_time > 0.22):
                  print('Turntable spin to right')
                  g.count_up(size-2)
              if(e.value > 0):
                g.count_up(size-1)
                print('Turntable spin to left')
                past_time = now_time
        except KeyboardInterrupt:
          print('Interrupt, quit.')
          g.quit()
          pygame.quit()
          break
    
[/code]


CC BY-SA 4.0
2020-02-10T03:47:45.193
62916
2020-02-10T11:44:04.940
2020-02-10T11:44:04.940
62882
answer