맨땅에 헤딩하는 사람

[Python] Windows GUI 자동화 pywinauto 사용법 본문

파이썬/이론

[Python] Windows GUI 자동화 pywinauto 사용법

purplechip 2020. 8. 22. 23:26

키움증권 Open API+에 수동 로그인하기 위해 GUI 윈도우 프로그램 창에서 로그인 정보를 입력해야 할 상황이 생겼다. Python은 pywinauto, pyautogui 등 다양한 GUI 자동화 모듈을 제공하는데 pyautogui가 실제 사용자 입장에서의 키보드, 마우스 움직임에 초점을 맞췄다면 pywinauto는 윈도우즈 프로그램 구성을 토대로 객체 지향적인 방식을 제공하는 느낌이다. 다양한 윈도우 환경에서 사용하기에 pywinauto가 좀 더 범용적으로 활용될 수 있을 것이라 생각하여 pywinauto를 채용하였고 여기에 그 원리와 사용법을 정리한다. 

 

모듈 사용 방법

pywinauto를 사용하는 방법은 아래에서 보듯 간단하다. 

  1. 조작하고자 하는 프로그램을 실행시키거나 연결한다.
  2. 실행되거나 연결된 프로그램의 WindowSpecification 객체를 받는다. (프로그램 안에 여러 윈도우 객체가 있는 셈, 메모장을 예로 들면 새 창, 검색창, 설정창 등)
  3. 해당 객체에 원하는 조작을 한다.

 

프로그램 실행, 연결

프로그램에 연결하는 방법은 Application().start()Application().connect()이다. 전자는 프로세스를 실행시키는 것이고 후자는 실행되어있는 프로세스를 연결하는 함수이다. Application() 선언 시 pywinauto의 backend를 설정할 수 있는데 프로그램 개발 시 사용된 GUI 프레임워크에 따라 다르다. pywin docs에서 아래와 같이 설명되어 있다.

  • Win32 API (backend="win32") - a default backend for now
    • MFC, VB6, VCL, simple WinForms controls and most of the old legacy apps
  • MS UI Automation (backend="uia")
    • WinForms, WPF, Store apps, Qt5, browsers

추가적으로 uia를 사용하는 것이 pywinauto에서 다룰 수 있는 윈도우가 더 많은 듯 하다. 이러한 차이점과 윈도우에 대한 것은 뒤에서 좀 더 설명하기로 하고 프로그램 연결, 실행에 대한 다음 코드를 보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
from pywinauto.application import Application
app = Application(backend="uia").start('notepad.exe'# process 실행
 
procs = pywinauto.findwindows.find_elements()
''
pywinauto.findwindows.find_elements() 실행 결과
 
[<win32_element_info.HwndElementInfo - '', Shell_TrayWnd, 65792>,
 <win32_element_info.HwndElementInfo - 'Spyder (Python 3.6)', Qt5QWindowIcon, 68182>,
 <win32_element_info.HwndElementInfo - '*제목 없음 - Windows 메모장', Notepad, 1771640>,
 <win32_element_info.HwndElementInfo - 'KOA StudioSA ver 2.20', AfxFrameOrView100, 1116620>,
 <win32_element_info.HwndElementInfo - 'Anaconda Prompt (py36_32) - python', ConsoleWindowClass, 264856>,
 <win32_element_info.HwndElementInfo - '카카오톡', EVA_Window_Dblclk, 196812>,
 <win32_element_info.HwndElementInfo - 'KOAStudioSA', CabinetWClass, 1116508>,
 <win32_element_info.HwndElementInfo - 'Microsoft Text Input Application', Windows.UI.Core.CoreWindow, 133404>,
 <win32_element_info.HwndElementInfo - '', DummyDWMListenerWindow, 65984>,
 <win32_element_info.HwndElementInfo - '', EdgeUiInputTopWndClass, 65974>,
 <win32_element_info.HwndElementInfo - '', DummyDWMListenerWindow, 65936>,
 <win32_element_info.HwndElementInfo - '', DummyDWMListenerWindow, 65934>,
 <win32_element_info.HwndElementInfo - '', Internet Explorer_Hidden, 329682>,
 <win32_element_info.HwndElementInfo - '', Windows.UI.Core.CoreWindow, 67630>,
 <win32_element_info.HwndElementInfo - '설정', ApplicationFrameWindow, 196860>,
 <win32_element_info.HwndElementInfo - '', Windows.UI.Core.CoreWindow, 67620>,
 <win32_element_info.HwndElementInfo - 'Microsoft Store', ApplicationFrameWindow, 67602>,
 <win32_element_info.HwndElementInfo - 'Program Manager', Progman, 131416>]
'''
 
for proc in procs:
    if proc.name == '*제목 없음 - Windows 메모장':
        break
app = Application(backend="uia").connect(process=proc.process_id) # process 연결
app = Application(backend='uia').connect(title="*제목 없음 - Windows 메모장"# 또 다른 process 연결방법
cs
  • Application.start() : 프로세스의 경로 및 프로세스의 이름을 넣어 실행해준다.
  • pywinauto.findwindows.find_elements() : 현재 윈도우 화면에 있는 프로세스 목록 리스트를 반환한다. 리스트의 각 요소는 element 객체로 프로세스 id, 핸들값, 이름 등의 정보를 보유한다.  
  • Application.connect() : 프로세스의 ID, 이름, 핸들값 등의 정보로 프로세스를 연결한다.

연결은 굉장히 쉽다. 실행시키거나, findwindows.find_elements() 함수를 통해 알게 된 프로세스의 정보 혹은 이름 자체를 연결시켜주면 된다. 개인적으로는 다른 프로그램과 연동했을 때 실행되었는 지 findwindows.find_elements()로 확인한 다음 프로세스 id를 사용해 연결하는 방법을 선호한다. 프로세스가 존재하지 않을 때 연결하는 것은 프로그램에 에러를 불러온다. findwindows.find_element()란 함수도 존재하는데 (find_elements() 함수에서 해당 이름, 클래스 혹은 프로세스 id 등에 해당하는 element 객체만을 불러오는 함수) title로 parmeter를 지정했더니 에러가 발생하여 사용을 할 수 없었다. 여하튼 이렇게 프로세스 객체를 얻었다면 다음은 Window 객체를 얻어야 한다.

 

WindowSpecification 객체 할당

WindowSpecification 객체란 윈도우 화면에 표시되는 창을 의미한다. 위에서도 설명했듯이 메모장을 실행시키면 메모장이 있고 메모장에 다른 이름으로 저장이나 찾기 등의 메뉴를 클릭하면 또 다른 창이 생성된다. 이러한 각각의 창 객체를 사용해서 원하는 동작을 수행할 수 있다. 객체를 할당하는 방법은 간단하다. 일단 메인 윈도우는 창의 Title을 사용하면 쉽게 획득할 수 있다. 그 후 사용할 수 있는 창 객체는 print_control_identifiers() 함수를 통해 확인할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
from pywinauto.application import Application
import pywinauto
 
app = Application(backend="uia").start('notepad.exe')
# 아래 2가지 방법은 모두 같은 윈도우 객체를 받음
dig = app.제목없음Windows메모장
dig = app['제목 없음 - Windows 메모장']
dig.print_control_identifiers()
'''
Dialog - '제목 없음 - Windows 메모장'    (L821, T156, R1669, B686)
['제목 없음 - Windows 메모장', 'Dialog', '제목 없음 - Windows 메모장Dialog']
child_window(title="제목 없음 - Windows 메모장", control_type="Window")
   | 
   | Edit - '텍스트 편집'    (L829, T207, R1661, B656)
   | ['Edit']
   | child_window(title="텍스트 편집", auto_id="15", control_type="Edit")
   |    | 
   |    | ScrollBar - '세로'    (L1644, T207, R1661, B656)
   |    | ['세로', 'ScrollBar', '세로ScrollBar']
   |    | child_window(title="세로", auto_id="NonClientVerticalScrollBar", control_type="ScrollBar")
   |    |    | 
   |    |    | Button - '위쪽 스크롤 화살표'    (L1644, T207, R1661, B224)
   |    |    | ['위쪽 스크롤 화살표', '위쪽 스크롤 화살표Button', 'Button', 'Button0', 'Button1']
   |    |    | child_window(title="위쪽 스크롤 화살표", auto_id="UpButton", control_type="Button")
   |    |    | 
   |    |    | Button - '아래쪽 스크롤 화살표'    (L1644, T639, R1661, B656)
   |    |    | ['아래쪽 스크롤 화살표Button', 'Button2', '아래쪽 스크롤 화살표']
   |    |    | child_window(title="아래쪽 스크롤 화살표", auto_id="DownButton", control_type="Button")
   | 
   | StatusBar - '상태 표시줄'    (L829, T656, R1661, B678)
   | ['상태 표시줄', 'StatusBar', '상태 표시줄StatusBar']
   | child_window(title="상태 표시줄", auto_id="1025", control_type="StatusBar")
   |    | 
   |    | Static - ''    (L829, T658, R1231, B678)
   |    | ['Static', 'Static0', 'Static1']
   |    | 
   |    | Static - '  Ln 1, Col 1'    (L1233, T658, R1371, B678)
   |    | ['Static2', '  Ln 1, Col 1', '  Ln 1, Col 1Static']
   |    | child_window(title="  Ln 1, Col 1", control_type="Text")
   |    | 
   |    | Static - ' 100%'    (L1373, T658, R1421, B678)
   |    | ['Static3', ' 100%', ' 100%Static']
   |    | child_window(title=" 100%", control_type="Text")
   |    | 
   |    | Static - ' Windows (CRLF)'    (L1423, T658, R1541, B678)
   |    | ['Static4', ' Windows (CRLF)Static', ' Windows (CRLF)']
   |    | child_window(title=" Windows (CRLF)", control_type="Text")
   |    | 
   |    | Static - ' UTF-8'    (L1543, T658, R1645, B678)
   |    | ['Static5', ' UTF-8', ' UTF-8Static']
   |    | child_window(title=" UTF-8", control_type="Text")
   | 
   | TitleBar - ''    (L845, T159, R1661, B187)
   | ['TitleBar']
   |    | 
   |    | Menu - '시스템'    (L829, T164, R851, B186)
   |    | ['Menu', '시스템Menu', '시스템', '시스템0', '시스템1', 'Menu0', 'Menu1']
   |    | child_window(title="시스템", auto_id="MenuBar", control_type="MenuBar")
   |    |    | 
   |    |    | MenuItem - '시스템'    (L829, T164, R851, B186)
   |    |    | ['MenuItem', '시스템MenuItem', '시스템2', 'MenuItem0', 'MenuItem1']
   |    |    | child_window(title="시스템", control_type="MenuItem")
   |    | 
   |    | Button - '최소화'    (L1522, T157, R1569, B187)
   |    | ['최소화Button', '최소화', 'Button3']
   |    | child_window(title="최소화", control_type="Button")
   |    | 
   |    | Button - '최대화'    (L1569, T157, R1615, B187)
   |    | ['최대화', 'Button4', '최대화Button']
   |    | child_window(title="최대화", control_type="Button")
   |    | 
   |    | Button - '닫기'    (L1615, T157, R1662, B187)
   |    | ['닫기', '닫기Button', 'Button5']
   |    | child_window(title="닫기", control_type="Button")
   | 
   | Menu - '응용 프로그램'    (L829, T187, R1661, B206)
   | ['Menu2', '응용 프로그램', '응용 프로그램Menu']
   | child_window(title="응용 프로그램", auto_id="MenuBar", control_type="MenuBar")
   |    | 
   |    | MenuItem - '파일(F)'    (L829, T187, R881, B206)
   |    | ['파일(F)', '파일(F)MenuItem', 'MenuItem2']
   |    | child_window(title="파일(F)", control_type="MenuItem")
   |    | 
   |    | MenuItem - '편집(E)'    (L881, T187, R933, B206)
   |    | ['편집(E)MenuItem', '편집(E)', 'MenuItem3']
   |    | child_window(title="편집(E)", control_type="MenuItem")
   |    | 
   |    | MenuItem - '서식(O)'    (L933, T187, R988, B206)
   |    | ['서식(O)', 'MenuItem4', '서식(O)MenuItem']
   |    | child_window(title="서식(O)", control_type="MenuItem")
   |    | 
   |    | MenuItem - '보기(V)'    (L988, T187, R1042, B206)
   |    | ['MenuItem5', '보기(V)MenuItem', '보기(V)']
   |    | child_window(title="보기(V)", control_type="MenuItem")
   |    | 
   |    | MenuItem - '도움말(H)'    (L1042, T187, R1109, B206)
   |    | ['MenuItem6', '도움말(H)', '도움말(H)MenuItem']
   |    | child_window(title="도움말(H)", control_type="MenuItem")
'''
# backend='win32'인 경우
'''
Notepad - '제목 없음 - Windows 메모장'    (L821, T156, R1669, B686)
['제목 없음 - Windows 메모장', 'Notepad', '제목 없음 - Windows 메모장Notepad']
child_window(title="제목 없음 - Windows 메모장", class_name="Notepad")
   | 
   | Edit - ''    (L829, T207, R1661, B656)
   | ['Edit', '제목 없음 - Windows 메모장Edit']
   | child_window(class_name="Edit")
   | 
   | StatusBar - ''    (L829, T656, R1661, B678)
   | ['StatusBar', 'StatusBar UTF-8', 'StatusBar Windows (CRLF)', 'StatusBar 100%', '제목 없음 - Windows 메모장StatusBar', 'StatusBar  Ln 1, Col 1']
   | child_window(class_name="msctls_statusbar32")
'''
cs

윈도우 객체는 위와 같이 두 가지 방식으로 얻을 수 있다. 이 모듈에서 흥미로운 점이 바로 이것이다. 윈도우가 트리 구조로 이루어져 있어 조작이 간편해진다. 즉 dig = app.제목없음Windows메모장.Edit.type_keys("message") 와 같은 형태로 조작이 가능한 것이다. 괄호 '[]' 안에 들어가 있는 문자열을 위와 같이 하위 호출로 사용할 수 있다. 다만 app.제목없음Windows메모장 과 같이 사용하기 위해서는 윈도우 이름을 그대로 사용하는 것이 아니라 스페이스, 콤마, 그 외 특수 기호들을 배제한 채로 사용해야 한다. 그리고 윈도우 객체는 control_type이 여러가지인데 각각에 맞는 동작을 수행한다. window인 경우는 말 그대로 검색창 등의 창을 의미하고 이 창에서 Button이나 Edit으로 연결되어 클릭이나 텍스트 출력 등의 기능을 수행한다. 

 

print_control_identifiers() 함수를 사용하면 다음과 같이 현재 window에서 호출할 수 있는 window를 tree 형태로 보여준다. 위에서 'uia'와 'win32'에 대해 잠깐 언급했는데 메모장을 두 가지 backend로 실행시켜서 참조할 수 있는 객체를 살펴보면 차이를 확인할 수 있는데 프로그램이 어떤 방식으로 구현되었느냐에 따른 결과인 듯 하다. 물론 메모장은 'uia'가 많은 기능을 수행할 수 있지만 MFC 등으로 구현된 프로그램은 'win32'에서 더 많은 기능을 가질 수도 있을 것이라 추측된다. 그럼 마지막으로 윈도우에서 간단한 조작을 알아보자. 

 

WindowSpecification 객체 조작

간단하게 클릭, 키 입력 등에 대한 조작 코드는 다음과 같다.

1
2
3
# 위 코드에서 연결
dig.Edit.type_keys('pywinauto{ENTER}test')
dig['최대화Button'].click()
cs
  • type_keys() : Edit 창에 문자열을 전송하는 기능으로 {ENTER}와 같은 특수 키도 입력이 가능하다.
  • click() : 좌클릭하는 함수, parameter에 따라 우클릭이나 다른 기능도 가능하다.

위 코드를 실행시키면 다음과 같이 메모장이 동작한다.

[그림 1. 메모장 메시지 출력 및 화면 최대화]

이와 같은 방식으로 파이썬에서 Windows GUI를 조작할 수 있다.

 

다만 type_keys()click() 같은 함수는 윈도우가 최상단, 즉 모니터 화면의 가장 앞에 보여야 제대로 동작한다. 이에 대한 해결책으로 Edit에 한해 send_keystrokes()set_edit_text() 같은 함수를 제공하는데 제대로 동작하는 지 검증은 해보지 않았다. 또한 이 외에 다양한 control_type과 더 강력한 기능들이 있으므로 자세한 내용은 아래 pywinauto docs를 참고바란다.

 

참고

pywinauto docs

https://pywinauto.readthedocs.io/en/latest/remote_execution.html 

Comments