use ctypes to call windows api

繼續之前使用ctypes實做Windows data type,這次要做的是使用windows api,來做一個console interface的聊天室,此篇僅先做出介面的樣子

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
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
	import ctypes  
from ctypes import wintypes
from ctypes.wintypes import *
import msvcrt
import sys

#-----------------debug use----------------------
import inspect

def myDebugMsg(msg=''):
print('{} at:{}'.format(msg, inspect.stack()[1][1:3]))

def pause():
while True:
if msvcrt.kbhit():
break
#------------------------------------------------


#--------------------------------------------------
# use ctypes to create a windows data type
class Char(ctypes.Union):
_fields_ = [("UnicodeChar",WCHAR),
("AsciiChar", CHAR)]

class CHAR_INFO(ctypes.Structure):
_anonymous_ = ("Char",)
_fields_ = [("Char", Char),
("Attributes", WORD)]


class CONSOLE_CURSOR_INFO(ctypes.Structure):
_fields_ = [('dwSize', DWORD),
('bVisible', BOOL)]


PCHAR_INFO = ctypes.POINTER(CHAR_INFO)
COORD = wintypes._COORD
#---------------------------------------------------


class dllLoader:
'''
load the dll written by c and call them for use
'''
def __init__(self):
self.mKernel32 = ctypes.WinDLL('Kernel32.dll')
self.mUser32 = ctypes.WinDLL('User32.dll')

def getKernel32(self):
return self.mKernel32

def getUser32(self):
return self.mUser32

Kernel32 = property(getKernel32)
User32 = property(getUser32)


dll = dllLoader()
```

上面這些code是先將我會用到的dll檔先load好和windows api data type實做出來,另外還有方便我用來debug的函式,接下來就是用來實做介面的code

```python
class consoleBackBuffer:
def __init__(self, w, h):
self.mstdout = dll.Kernel32.CreateConsoleScreenBuffer(
0x80000000|0x40000000, #generic read and write
0x00000001|0x00000002,
None,
1, #CONSOLE_TEXTMODE_BUFFER defined in winbase.h
None)
if self.mstdout == HANDLE(-1):
myDebugMsg('CreateConsoleScreenBuffer failed')


self.cursorInfo = CONSOLE_CURSOR_INFO()
self.cursorInfo.dwSize = 25


self.coordBufSize = COORD()
self.coordBufSize.X = w
self.coordBufSize.Y = h

self.coordBufCoord = COORD()
self.coordBufCoord.X = 0
self.coordBufCoord.y = 0


self.readRgn = SMALL_RECT()
self.readRgn.Top = 0
self.readRgn.Left = 0
self.readRgn.Right = w-1
self.readRgn.Bottom = h-1


self.writeRgn = SMALL_RECT()
self.writeRgn.Top = 0
self.writeRgn.Left = 0
self.writeRgn.Right = 79
self.writeRgn.Bottom = 24

self.actuallyWritten = DWORD() # used when writeconsole called

self.setCursorVisibility() # by default, set cursor invisible

def setCursorVisibility(self, flag = False):
self.cursorInfo.bVisible = flag
dll.Kernel32.SetConsoleCursorInfo(self.mstdout, ctypes.byref(self.cursorInfo))

def toggleActiveConsole(self, stdout=None):
if stdout != None:
dll.Kernel32.SetConsoleActiveScreenBuffer(stdout)
else:
dll.Kernel32.SetConsoleActiveScreenBuffer(self.mstdout)

def getHandle(self):
return self.mstdout

def set_color(self, color):
dll.Kernel32.SetConsoleTextAttribute( self.mstdout, color )


def gotoxy(self, x, y, stdout=None):
coord = COORD(x, y)
if stdout == None:
dll.Kernel32.SetConsoleCursorPosition( self.mstdout, coord)
else:
dll.Kernel32.SetConsoleCursorPosition( stdout, coord)

def setWriteSrc(self, x, y):
self.writeRgn.Top = y
self.writeRgn.Left = x
self.writeRgn.Right = x+self.coordBufSize.X-1
self.writeRgn.Bottom = y+self.coordBufSize.Y-1

def write(self, msg):
while len(msg)>self.coordBufSize.X:

tempMsg = msg[:self.coordBufSize.X-1]+'\n'
msg = msg[self.coordBufSize.X-1:]

success = dll.Kernel32.WriteConsoleW(
self.mstdout,
tempMsg,
DWORD(len(tempMsg)),
ctypes.byref(self.actuallyWritten),
None)

if success == 0:
myDebugMsg('WriteConsoleW failed')

if '\n' not in msg:
msg = msg+'\n'

if len(msg) != 0:
success = dll.Kernel32.WriteConsoleW(
self.mstdout,
msg,
DWORD(len(msg)),
ctypes.byref(self.actuallyWritten),
None)

if success == 0:
myDebugMsg('WriteConsoleW failed')

def present(self, mainBuffer):
chiBuffer = (CHAR_INFO*(self.coordBufSize.X*self.coordBufSize.Y))()

success = dll.Kernel32.ReadConsoleOutputW(
self.mstdout,
ctypes.byref(chiBuffer),
self.coordBufSize,
self.coordBufCoord,
ctypes.byref(self.readRgn)
)

if success == 0:
myDebugMsg('ReadConsoleOutputW failed')

success = dll.Kernel32.WriteConsoleOutputW(
mainBuffer,
ctypes.byref(chiBuffer),
self.coordBufSize,
self.coordBufCoord,
ctypes.byref(self.writeRgn))

if success == 0:
myDebugMsg('WriteConsoleOutput failed')

我會實做一個console buffer的原因是,我想要用double buffer,畢竟畫面需要常常更新,為了不讓畫面有閃爍,因此使用這個技術。題外話,其實感覺滿新鮮的 :) 因為我以前用過double buffer是在使用 windows api的gdi才用過,果然在CLI裡也有 :) 親自實做的感覺,其實滿有成就感的,接下來就是做一些 widget出來,只是我對於GUI的元件該有怎樣的包裝,我也不清楚,因此只是隨意的做做而已 :(

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
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
class widget:  
def __init__(self, sx, sy, w, h):
self.console = consoleBackBuffer(w, h)
self.console.setWriteSrc(sx, sy)
self.gx = sx
self.gy = sy
#global position
self.w = w
self.h = h
self.mTitle = 'no name'
self.content = []

def getConsole(self):
return self.console

def getContent(self):
return self.content[:]

def setContent(self, content):
self.content = content[:]


def setTitle(self, title):
self.mTitle = title

def getTitle(self):
return self.mTitle

title = property(getTitle, setTitle)

def addContent(self, s):
if len(s) >= self.w-3:
self.content.append(s[:self.w-3])
self.content.append(s[self.w-3:])
else:
self.content.append(s)

def delContent(self, s):
self.content.remove(s)

def display(self, stdout):
self.console.present(stdout)


class usermenu(widget):
'''
show user list
'''
def __init__(self, sx, sy, w, h):
super().__init__(sx, sy, w, h)

def update(self):
#draw outline
border = '|'+'-'*(self.w-2)+'|'
emptyLine = '|'+' '*(self.w-2)+'|'
for y in range(self.h):
if y in (0, 2, self.h-1):
self.console.write(border)
else:
self.console.write(emptyLine)

#draw title and user list
tx = int( (self.w - len(self.title))/2 )
self.console.gotoxy(tx, 1)
self.console.write(self.title)


for i in range(len(self.content)):
self.console.gotoxy(2, 3+i)
self.console.write(self.content[i])
self.console.gotoxy(0, 0)


class msgroom(widget):
def __init__(self, sx, sy, w, h):
super().__init__(sx, sy, w, h)
self.scroll = 0
self.start = 0

def scrollContent(self, offset):
self.scroll += offset
if self.scroll > 0:
self.scroll = 0

def detectPageUpAndDown(self):
pageUP = dll.User32.GetAsyncKeyState(0x21)
pageDown = dll.User32.GetAsyncKeyState(0x22)

if pageUP != 0:
self.scrollContent(-2)
if pageDown != 0:
self.scrollContent(2)

def update(self):
self.detectPageUpAndDown()
border = '|'+'-'*(self.w-2)+'|'
emptyLine = '|'+' '*(self.w-2)+'|'

for y in range(self.h):
if y in (0, 2, self.h-1):
self.console.write(border)
else:
self.console.write(emptyLine)
#draw title and content
tx = int( (self.w - len(self.title))/2 )
self.console.gotoxy(tx, 1)
self.console.write(self.title)

index = len(self.content)-(self.h-4)+self.scroll
self.start = 0 if index < 0 else index

obj = self.content if len(self.content) < self.h-4 else self.content[self.start : self.start+self.h-4]
# wow, slice in python seems to automatically check the index if it is out of range
# ex. lst = [1,2,3,4,5]
# lst[-6:] no error produce

for i in range(len(obj)):
self.console.gotoxy(1, 3+i)
self.console.write(obj[i])
self.console.gotoxy(0, 0)



class inputLabel(widget):
def __init__(self, sx, sy, w, h):
super().__init__(sx, sy, w, h)

def update(self):
border = '|'+'-'*(self.w-2)+'|'
emptyLine = '|'+' '*(self.w-2)+'|'

for y in range(self.h):
if y in (0, 2, self.h-1):
self.console.write(border)
else:
self.console.write(emptyLine)

tx = int( (self.w - 8))
self.console.gotoxy(tx, 0)
self.console.write('|')
self.console.gotoxy(tx, 2)
self.console.write('|')
self.console.gotoxy(tx, 1)
self.console.write('|submit')
self.console.gotoxy(0, 0)
好了到這邊為止任務就完成了,想當然爾,要寫個code來測試,自己寫出來的code是不是跟自己想的效果一樣
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
def test():  
'''
like this?
in windows, the size of console is 80X25 by default

setting:
2 space for left and rigth edge
one space or \n for each panel

chatroom: 15 height, 55 width
user list: 16 height, 15 width
submit: 71 width, height 3
|-------------------------------------------------------------| |-----------|
| chatroom | | user list |
|-------------------------------------------------------------| |-----------|
|name2: hello, you | |df |
|name1: yo | |yeah |
| | |one |
| | | |
| | | |
|-------------------------------------------------------------| |-----------|

|-------------------------------------------------------------------|-------|
| yo, hahaha |submit |
|-------------------------------------------------------------------|-------|

'''
from threading import Timer, Thread


hstdout = dll.Kernel32.GetStdHandle(DWORD(-11))

if(hstdout == HANDLE(-1)):
print('create buffer failed')

backBuffer = consoleBackBuffer(80, 25)

userpanel = usermenu(56, 0, 15, 17)
userpanel.title = 'user list'
userpanel.addContent('heyhey')
userpanel.addContent('haha')
userpanel.update()
userpanel.display(backBuffer.getHandle())

chatroom = msgroom(0, 0, 55, 17)
chatroom.title = 'chat room'
chatroom.addContent("eric: I'm so happy")
chatroom.update()
chatroom.display(backBuffer.getHandle())

inLabel = inputLabel(0, 18, 71, 3)
inLabel.update()
inLabel.display(backBuffer.getHandle())

backBuffer.present(hstdout)
input()



if __name__ == '__main__':
test()

結果跑出來的,如同我想像 happy :)
其實後來我有使用socket,thread模組,加上這篇寫的code實做出一個多人聊天室,只是感覺鳥鳥的,有點不想PO出來 :( ,最近是打算寫出一個聊天軟體是GUI介面的,所以要先來研究tkinter,希望能夠成功做出來。