2013年7月13日 星期六

use ctypes to call windows api

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



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

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的元件該有怎樣的包裝,我也不清楚,因此只是隨意的做做而已 :(

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是不是跟自己想的效果一樣
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,希望能夠成功做出來。

1 則留言: