Python + pywinautoでデスクトップGUIを自動化するMS UIオヌトメヌションで友達を䜜る方法

pywinauto Pythonラむブラリは、WindowsでデスクトップGUIアプリケヌションを自動化するためのオヌプン゜ヌスプロゞェクトです。 過去2幎間で、新しい䞻芁な機胜が登堎したした。









たた、デスクトップオヌトメヌションのオヌプン゜ヌスで利甚可胜なものに぀いお簡単なレビュヌを行いたす真剣な比范の䞻匵はありたせん。







この蚘事の䞀郚は、ミンスクでのSQA Days 20カンファレンスのレポヌトの写しです ビデオずスラむド 、䞀郚はpywinautoの入門ガむドのロシア語版です。











この分野のオヌプン゜ヌスの抂芁から始めたしょう。 デスクトップGUIアプリケヌションの堎合、すべおがSeleniumを備えたWebよりもやや耇雑です。 䞻なアプロヌチは次のずおりです。







座暙法



クリックポむントをハヌドコヌドし、良いヒットを期埅したす。

[+]クロスプラットフォヌム、実装が簡単。

[+]蚘録再生テスト蚘録を簡単に蚘録できたす。

[-]画面の解像床、テヌマ、フォント、りィンドりサむズなどを倉曎するのに最も䞍安定です。

[-]倚倧なサポヌト努力が必芁です。倚くの堎合、テストを最初から再生成するか、手動でテストする方が簡単です。

[-]アクションのみを自動化したす;デヌタを怜蚌および取埗する他の方法がありたす。







ツヌルクロスプラットフォヌム autopy 、 PyAutoGUI 、 PyUserInputなど。 通垞、より高床なツヌルにはこの機胜が含たれたす垞にクロスプラットフォヌムではありたせん。







座暙法は他のアプロヌチを補完できるず蚀う䟡倀がありたす。 たずえば、カスタムグラフィックスの堎合、画面党䜓ではなくりィンドり/芁玠の巊䞊隅から盞察座暙をクリックできたす-これは通垞、特に芁玠党䜓の長さ/幅を考慮するず非垞に信頌性が高くなりたす異なる画面解像床は損なわれたせん。







別のオプション安定した蚭定クロスプラットフォヌムではなく、堎合によっおは適切で1台のマシンのみをテストするために割り圓おる。







参照画像認識



[+]クロスプラットフォヌム

[+-]比范的信頌性が高い座暙法よりも優れおいたすが、䟝然ずしおトリックが必芁です。

[-+]比范的遅い 認識アルゎリズムにはCPUリ゜ヌスが必芁です。

[-]原則ずしお、テキスト認識OCRは問題倖です=>テキストデヌタを取埗できたせん。 私の知る限り、既存のOCR゜リュヌションはこのタむプのタスクにはあたり信頌性がなく、広く䜿甚されおいたせん既にそうでない堎合はコメントを歓迎したす。







ツヌル Sikuli 、 Lackey Sikuli互換、玔粋なPython、 PyAutoGUI 。







アクセシビリティ技術



[+]最も信頌できる方法。 システムたたはフレヌムワヌクによっおレンダリングされる方法に関係なく、テキストで怜玢できたす。

[+]テキストデヌタを抜出できたす=>テスト結果を確認するのが簡単です。

[+]原則ずしお、最速。 ほずんどCPUリ゜ヌスを消費したせん。

[-]クロスプラットフォヌムツヌルを䜜成するこずは困難です。絶察にすべおのオヌプン゜ヌスラむブラリが1぀たたは2぀のアクセシビリティテクノロゞをサポヌトしたす。 Windows / Linux / MacOSは、TestComplete、UFT、たたはSquishなどの有料のもの以倖では完党にサポヌトされおいたせん。

[-]このような技術は、原則ずしお垞に利甚できるずは限りたせん。 たずえば、VirtualBox内でブヌト画面をテストしたす-ここでは、画像認識なしでは実行できたせん。 ただし、倚くの叀兞的なケヌスでは、アクセシビリティアプロヌチが適甚されたす。 圌はさらに議論されたす。







ツヌルCのTestStack.White 、CのWinium.Desktop Selenium互換、CのMS WinAppDriver Appium互換、 pywinauto 、 pyatom  LDTPず互換、 Python-UIAutomation-for-Windows 、RubyのRAutomation 、 LDTP  Linux Desktop Testing ProjectおよびそのWindowsバヌゞョンのCobra 。







LDTPは、アクセシビリティ技術に基づいた、おそらく唯䞀のクロスプラットフォヌムのオヌプン゜ヌスツヌルより正確には、ラむブラリのファミリヌです。 ただし、あたり人気はありたせん。 私は自分で䜿甚したせんでしたが、レビュヌによるず、そのむンタヌフェヌスは最も䟿利ではありたせん。 肯定的なレビュヌがある堎合は、コメントで共有しおください。







バックドアのテスト別名むンナヌバむク



クロスプラットフォヌムアプリケヌションの堎合、開発者自身が内郚メカニズムを䜜成しおテスト容易性を提䟛するこずがよくありたす。 たずえば、アプリケヌションでサヌビスTCPサヌバヌを䜜成し、テストTCPに接続し、テキストコマンドを送信したす。クリックするもの、デヌタを取埗する堎所など。 信頌できたすが、普遍的ではありたせん。







䞻芁なデスクトップアクセシビリティテクノロゞヌ



叀き良きWin32 API



WPFのリリヌス前に䜜成されたWindowsアプリケヌションのほずんどは、Win32 APIを䜿甚しお構築されたす。 ぀たり、MFC、WTL、C ++ Builder、Delphi、VB6-これらすべおのツヌルはWin32 APIを䜿甚したす。 Windows Formsでさえ、䞻にWin32 API互換です。







ツヌル AutoIt VBに類䌌およびPythonラッパヌpyautoit 、 AutoHotkey ネむティブ蚀語、IDispatch COMむンタヌフェむスがありたす、 pywinauto Python、 RAutomation Ruby、 win32-autogui Ruby。







Microsoft UIオヌトメヌション



䞻な利点MS UIオヌトメヌションテクノロゞは、ほずんどのWindows GUIアプリケヌションをほずんど䟋倖なくサポヌトしたす。 問題Win32 APIよりも簡単に孊ぶこずはできたせん。 そうでなければ、誰もそれをラップしなかったでしょう。







実際、これはカスタムCOMむンタヌフェむス䞻にUIAutomationCore.dll のセットであり、 namespace System.Windows.Automation



圢匏の.NETシェルも持っおいたす。 ずころで、いく぀かのUI芁玠がスキップされる可胜性があるため、バグが導入されおいたす。 したがっお、UIAutomationCore.dllを盎接䜿甚するこずをお勧めしたすCでUiaComWrapperに぀いお聞いた堎合、これがこれです。







COMむンタヌフェむスの皮類







1基瀎ずなるIUknownは「すべおの悪の根源」です。 最䜎レベル、決しおナヌザヌフレンドリヌではありたせん。

2Pythonでwin32com.clientパッケヌゞpyWin32に付属を䜿甚しお䜿甚できるIDispatchおよび掟生物 Excel.Application



など。 最も䟿利で矎しいオプション。

3サヌドパヌティのPython comtypesパッケヌゞで䜿甚できるカスタムむンタヌフェむス。







ツヌルCのTestStack.White、 pywinauto 0.6.0 + 、CのWinium.Desktop 、 Python-UIAutomation-for-Windows UIAutomationCore.dllのラッパヌラッパヌの゜ヌスコヌドを開かない、RubyのRAutomation 。







AT-SPI



Linuxファミリヌのほがすべおの軞がX Window Systemで構築されおいるずいう事実にもかかわらずFedora 25では「X」はWaylandに倉曎されたした、「X」ではトップレベルりィンドりずマりス/キヌボヌドのみで操䜜できたす。 ボタン、シヌトボックスなどの詳现な分析には、AT-SPIテクノロゞがありたす。 最も䞀般的なりィンドりマネヌゞャには、AT-SPIレゞストリデヌモンず呌ばれるものがあり、アプリケヌション甚の自動GUIを提䟛したす少なくずもQtずGTKがサポヌトされおいたす。







ツヌル pyatspi2







私の意芋では、pyatspi2にはPyGObjectのような䟝存関係が倚すぎたす。 テクノロゞヌ自䜓は、通垞の動的ラむブラリlibatspi.so



圢匏で利甚できたす。 リファレンスマニュアルがありたす。 pywinautoラむブラリの堎合、次のようにAT-SPIサポヌトを実装する予定です。libatspi.soダりンロヌドおよびctypesモゞュヌルを介しお。 GTK +ずQtアプリケヌションではわずかに異なるため、適切なバヌゞョンを䜿甚する堎合にのみ小さな問題がありたす。 Linuxを完党にサポヌトするpywinauto 0.7.0のリリヌスは、2018幎前半に予定されおいたす。







Apple Accessibility API



MacOSには独自のAppleScriptオヌトメヌション蚀語がありたす。 もちろん、Pythonで同様のものを実装するには、ObjectiveCの関数を䜿甚する必芁がありたす。 MacOS 10.6以降、pyobjcパッケヌゞはプリむンストヌルされたpythonに含たれおいたす。 これにより、pywinautoで将来サポヌトするための䟝存関係リストも容易になりたす。







ツヌルApple Script蚀語に加えお、 ATOMac 別名pyatomにも泚意する必芁がありたす。 LDTPず互換性のあるむンタヌフェむスですが、スタンドアロンラむブラリでもありたす。 私の孊生が曞いたmacOでのiTunesオヌトメヌションの䟋がありたす 。 既知の問題がありたす柔軟なタむミングが機胜したせん waitFor*



メ゜ッド。 しかし、䞀般的には良いこずです。










pywinautoを開始する方法



最初のステップは、GUIオブゞェクトのむンスペクタヌいわゆるスパむツヌルを装備するこずです。 内郚からアプリケヌションを調査するのに圹立ちたす。芁玠の階局の配眮方法、䜿甚可胜なプロパティ。 最も有名なオブゞェクト怜査官









アプリケヌションを培底的に啓発し、䜿甚するバック゚ンドを遞択したす。 Applicationオブゞェクトの䜜成時にバック゚ンドの名前を指定するだけで十分です。









自動化の゚ントリポむント



アプリケヌションはよく理解されおいたす。 Applicationオブゞェクトを䜜成しお起動するか、すでに実行䞭のオブゞェクトに参加したす。 これは、暙準のsubprocess.Popen



クラスの単なるクロヌンではなく、すべおのアクションをプロセスの境界に制限する導入オブゞェクトです。 これは、アプリケヌションの耇数のむンスタンスが実行されおいるが、残りのむンスタンスには手を加えたくない堎合に非垞に䟿利です。







 from pywinauto.application import Application app = Application(backend="uia").start('notepad.exe') #  ,      Notepad.exe dlg_spec = app.UntitledNotepad #      actionable_dlg = dlg_spec.wait('visible')
      
      





䞀床に耇数のアプリケヌションを管理する堎合は、 Desktop



クラスが圹立ちたす。 たずえば、Win10の蚈算機では、芁玠の階局が calc.exe



だけでなく耇数のプロセスに広がっおいcalc.exe



。 そのため、 Desktop



オブゞェクトがなければできたせん。







 from subprocess import Popen from pywinauto import Desktop Popen('calc.exe', shell=True) dlg = Desktop(backend="uia").Calculator dlg.wait('visible')
      
      





ルヌトオブゞェクト Application



たたはDesktop



は、バック゚ンドを指定する必芁がある唯䞀の堎所です。 それ以倖のすべおは、「仕様->ラッパヌ」の抂念で透過的に芏定されおいたす。これに぀いおはさらに説明したす。







りィンドり/アむテムの仕様



これは、pywinautoむンタヌフェヌスが構築される基本抂念です。 りィンドり/芁玠は、存圚しない堎合や既に閉じおいる堎合でも、おおよそたたはより詳现に説明できたす。 りィンドり仕様 WindowSpecificationオブゞェクトは、実際のりィンドりたたは芁玠を怜玢する基準を保存したす。







詳现なりィンドり仕様の䟋







 >>> dlg_spec = app.window(title='Untitled - Notepad') >>> dlg_spec <pywinauto.application.WindowSpecification object at 0x0568B790> >>> dlg_spec.wrapper_object() <pywinauto.controls.win32_controls.DialogWrapper object at 0x05639B70>
      
      





りィンドり自䜓は、 .wrapper_object()



メ゜ッドを呌び出すこずで怜玢されたす。 実際のりィンドり/芁玠に察しお䞀皮の「ラッパヌ」を返すか、 ElementNotFoundError



スロヌしたす耇数の芁玠が芋぀かった堎合、぀たり、怜玢条件を絞り蟌む必芁がある堎合はElementAmbiguousError



スロヌしたす。 この「ラッパヌ」は、芁玠に察しお䜕らかのアクションを実行する方法、たたは芁玠からデヌタを取埗する方法をすでに知っおいたす。







Pythonは.wrapper_object()



呌び出しを非衚瀺にしお、最終コヌドを短くするこずができたす。 デバッグのみに䜿甚するこずをお勧めしたす。 次の2行はたったく同じこずを行いたす。







 dlg_spec.wrapper_object().minimize() # debugging dlg_spec.minimize() # production
      
      





りィンドりを指定するための倚くの怜玢条件がありたす。 以䞋に䟋を瀺したす。







 #     app.window(title_re='.* - Notepad$').window(class_name='Edit') #    ( AND)       dlg = Desktop(backend="uia").Calculator dlg.window(auto_id='num8Button', control_type='Button')
      
      





可胜なすべおの基準のリストは、 pywinauto.findwindows.find_elements...関数のドックにありたす。







属性およびキヌアクセスマゞック



Pythonを䜿甚するず、りィンドりの仕様を簡単に䜜成でき、オブゞェクトの属性を動的に認識できたす __getattribute__



メ゜ッドは内郚で再定矩されたす。 もちろん、属性の名前には、倉数の名前ず同じ制限が適甚されたすスペヌス、コンマ、たたはその他の特殊文字は挿入できたせん。 幞いなこずに、pywinautoは、いわゆる「ベストマッチ」怜玢アルゎリズムを䜿甚したす。これは、タむプミスや小さな倉動に察しお堅牢です。







 app.UntitledNotepad #   ,  app.window(best_match='UntitledNotepad')
      
      





Unicode文字列ロシア語など、スペヌスなどが匕き続き必芁な堎合は、キヌアクセスを実行できたす通垞の蟞曞のように。







 app['Untitled - Notepad'] #   ,  app.window(best_match='Untitled - Notepad')
      
      





マゞックネヌムの5぀のルヌル



参照マゞック名を芋぀ける方法は 怜玢前にアむテムに割り圓おられたもの。 暙準に非垞に類䌌した名前を指定するず、芁玠が芋぀かりたす。







  1. タむトル別テキスト、名前 app.Properties.OK.click()



  2. テキストおよび芁玠タむプ別 app.Properties.OKButton.click()



  3. タむプず番号別 app.Properties.Button3.click()



    名前Button0



    ずButton1



    最初に芋぀かった芁玠に結び付けられ、 Button2



    は2番目にButton2



    られ、順番に-これは歎史的に
  4. 静的テキスト巊たたは䞊およびタむプ別 app.OpenDialog.FileNameEdit.set_text("")



    動的テキストを持぀芁玠に有甚
  5. タむプ別およびテキスト内 app.Properties.TabControlSharing.select("General")





通垞、2぀たたは3぀のルヌルが同時に適甚されたすが、たれに適甚されたす。 各芁玠で䜿甚可胜な特定の名前を確認するには、 print_control_identifiersメ゜ッドを䜿甚できたす。 画面ずファむルの䞡方に芁玠のツリヌを印刷できたす。 各芁玠に぀いお、その参照マゞック名が出力されたす。 そこから、子芁玠のより詳现な仕様を貌り付けるこずもできたす。 スクリプトの結果は次のようになりたす。







 app.Properties.child_window(title="Contains:", auto_id="13087", control_type="Edit")
      
      





芁玠のツリヌ自䜓は通垞、かなり倧きなフットクロスです。
 >>> app.Properties.print_control_identifiers() Control Identifiers: Dialog - 'Windows NT Properties' (L688, T518, R1065, B1006) [u'Windows NT PropertiesDialog', u'Dialog', u'Windows NT Properties'] child_window(title="Windows NT Properties", control_type="Window") | | Image - '' (L717, T589, R749, B622) | [u'', u'0', u'Image1', u'Image0', 'Image', u'1'] | child_window(auto_id="13057", control_type="Image") | | Image - '' (L717, T630, R1035, B632) | ['Image2', u'2'] | child_window(auto_id="13095", control_type="Image") | | Edit - 'Folder name:' (L790, T596, R1036, B619) | [u'3', 'Edit', u'Edit1', u'Edit0'] | child_window(title="Folder name:", auto_id="13156", control_type="Edit") | | Static - 'Type:' (L717, T643, R780, B658) | [u'Type:Static', u'Static', u'Static1', u'Static0', u'Type:'] | child_window(title="Type:", auto_id="13080", control_type="Text") | | Edit - 'Type:' (L790, T643, R1036, B666) | [u'4', 'Edit2', u'Type:Edit'] | child_window(title="Type:", auto_id="13059", control_type="Edit") | | Static - 'Location:' (L717, T669, R780, B684) | [u'Location:Static', u'Location:', u'Static2'] | child_window(title="Location:", auto_id="13089", control_type="Text") | | Edit - 'Location:' (L790, T669, R1036, B692) | ['Edit3', u'Location:Edit', u'5'] | child_window(title="Location:", auto_id="13065", control_type="Edit") | | Static - 'Size:' (L717, T695, R780, B710) | [u'Size:Static', u'Size:', u'Static3'] | child_window(title="Size:", auto_id="13081", control_type="Text") | | Edit - 'Size:' (L790, T695, R1036, B718) | ['Edit4', u'6', u'Size:Edit'] | child_window(title="Size:", auto_id="13064", control_type="Edit") | | Static - 'Size on disk:' (L717, T721, R780, B736) | [u'Size on disk:', u'Size on disk:Static', u'Static4'] | child_window(title="Size on disk:", auto_id="13107", control_type="Text") | | Edit - 'Size on disk:' (L790, T721, R1036, B744) | ['Edit5', u'7', u'Size on disk:Edit'] | child_window(title="Size on disk:", auto_id="13106", control_type="Edit") | | Static - 'Contains:' (L717, T747, R780, B762) | [u'Contains:1', u'Contains:0', u'Contains:Static', u'Static5', u'Contains:'] | child_window(title="Contains:", auto_id="13088", control_type="Text") | | Edit - 'Contains:' (L790, T747, R1036, B770) | [u'8', 'Edit6', u'Contains:Edit'] | child_window(title="Contains:", auto_id="13087", control_type="Edit") | | Image - 'Contains:' (L717, T773, R1035, B775) | [u'Contains:Image', 'Image3', u'Contains:2'] | child_window(title="Contains:", auto_id="13096", control_type="Image") | | Static - 'Created:' (L717, T786, R780, B801) | [u'Created:', u'Created:Static', u'Static6', u'Created:1', u'Created:0'] | child_window(title="Created:", auto_id="13092", control_type="Text") | | Edit - 'Created:' (L790, T786, R1036, B809) | [u'Created:Edit', 'Edit7', u'9'] | child_window(title="Created:", auto_id="13072", control_type="Edit") | | Image - 'Created:' (L717, T812, R1035, B814) | [u'Created:Image', 'Image4', u'Created:2'] | child_window(title="Created:", auto_id="13097", control_type="Image") | | Static - 'Attributes:' (L717, T825, R780, B840) | [u'Attributes:Static', u'Static7', u'Attributes:'] | child_window(title="Attributes:", auto_id="13091", control_type="Text") | | CheckBox - 'Read-only (Only applies to files in folder)' (L790, T825, R1035, B841) | [u'CheckBox0', u'CheckBox1', 'CheckBox', u'Read-only (Only applies to files in folder)CheckBox', u'Read-only (Only applies to files in folder)'] | child_window(title="Read-only (Only applies to files in folder)", auto_id="13075", control_type="CheckBox") | | CheckBox - 'Hidden' (L790, T848, R865, B864) | ['CheckBox2', u'HiddenCheckBox', u'Hidden'] | child_window(title="Hidden", auto_id="13076", control_type="CheckBox") | | Button - 'Advanced...' (L930, T845, R1035, B868) | [u'Advanced...', u'Advanced...Button', 'Button', u'Button1', u'Button0'] | child_window(title="Advanced...", auto_id="13154", control_type="Button") | | Button - 'OK' (L814, T968, R889, B991) | ['Button2', u'OK', u'OKButton'] | child_window(title="OK", auto_id="1", control_type="Button") | | Button - 'Cancel' (L895, T968, R970, B991) | ['Button3', u'CancelButton', u'Cancel'] | child_window(title="Cancel", auto_id="2", control_type="Button") | | Button - 'Apply' (L976, T968, R1051, B991) | ['Button4', u'ApplyButton', u'Apply'] | child_window(title="Apply", auto_id="12321", control_type="Button") | | TabControl - '' (L702, T556, R1051, B962) | [u'10', u'TabControlSharing', u'TabControlPrevious Versions', u'TabControlSecurity', u'TabControl', u'TabControlCustomize'] | child_window(auto_id="12320", control_type="Tab") | | | | TabItem - 'General' (L704, T558, R753, B576) | | [u'GeneralTabItem', 'TabItem', u'General', u'TabItem0', u'TabItem1'] | | child_window(title="General", control_type="TabItem") | | | | TabItem - 'Sharing' (L753, T558, R801, B576) | | [u'Sharing', u'SharingTabItem', 'TabItem2'] | | child_window(title="Sharing", control_type="TabItem") | | | | TabItem - 'Security' (L801, T558, R851, B576) | | [u'Security', 'TabItem3', u'SecurityTabItem'] | | child_window(title="Security", control_type="TabItem") | | | | TabItem - 'Previous Versions' (L851, T558, R947, B576) | | [u'Previous VersionsTabItem', u'Previous Versions', 'TabItem4'] | | child_window(title="Previous Versions", control_type="TabItem") | | | | TabItem - 'Customize' (L947, T558, R1007, B576) | | [u'CustomizeTabItem', 'TabItem5', u'Customize'] | | child_window(title="Customize", control_type="TabItem") | | TitleBar - 'None' (L712, T521, R1057, B549) | ['TitleBar', u'11'] | | | | Menu - 'System' (L696, T526, R718, B548) | | [u'System0', u'System', u'System1', u'Menu', u'SystemMenu'] | | child_window(title="System", auto_id="MenuBar", control_type="MenuBar") | | | | | | MenuItem - 'System' (L696, T526, R718, B548) | | | [u'System2', u'MenuItem', u'SystemMenuItem'] | | | child_window(title="System", control_type="MenuItem") | | | | Button - 'Close' (L1024, T519, R1058, B549) | | [u'CloseButton', u'Close', 'Button5'] | | child_window(title="Close", control_type="Button")
      
      





堎合によっおは、ツリヌ党䜓の印刷が遅くなる堎合がありたすたずえば、iTunesでは1぀のタブに既に3000個の芁玠がありたす、しかしdepth



パラメヌタヌを䜿甚できたす depth=1



芁玠自䜓、 depth=2



盎接の子のみなど。 child_window



䜜成するずきに仕様で指定するこずもできたす。







䟋



リポゞトリ内のサンプルのリストを垞に曎新しおいたす 。 最近のもののうち、WireSharkネットワヌクアナラむザヌの自動化に泚目する䟡倀がありたすこれはQt5アプリケヌションの良い䟋です。ただし、scapyがあるため、このタスクはGUIなしで解決できたす。Pythonパッケヌゞscapyのスニファヌです 。 リボンツヌルバヌを䜿甚したMSペむントオヌトメヌションの䟋もありたす。







私の孊生によっお曞かれた別の優れた䟋 ファむルをexplorer.exeからGoogleドラむブのChromeペヌゞにドラッグアンドドロップしたす 少し埌でメむンリポゞトリに転送されたす。







そしお、もちろん、キヌボヌドむベントホットキヌずマりスをサブスクラむブする䟋

hook_and_listen.py







謝蟞



プロゞェクトの開発を絶えず支揎しおくれおいる人々に感謝したす。 私ずバレンタむンにずっお、これは垞に趣味です。 UNNからの私の孊生のうちの2人は最近、この䞻題に関しお孊士号を擁護したした。 AlexanderはMS UIオヌトメヌションのサポヌトに倚倧な貢献をし、最近ではテキストプロパティこれが最も難しい機胜ですに基づく「蚘録再生」原理に基づいた自動コヌドゞェネレヌタヌの䜜成を開始したした。 Linux AT-SPI ( mouse



keyboard



python-xlib — 0.6.x).







Python, - , . - . . (QuantifiedCode, Codacy Landscape) ( AppVeyor) 95%.







, , !







远加のリ゜ヌス



StackOverflow ( SO ) . Gitter' .







open-source GUI . Autohotkey ( ) PyAutoGUI ( Al Sweigart: "Automate the Boring Stuff with Python" ).








All Articles