사용자 도구

사이트 도구


project:peekwindow

차이

문서의 선택한 두 판 사이의 차이를 보여줍니다.

차이 보기로 링크

양쪽 이전 판이전 판
다음 판
이전 판
project:peekwindow [2015/08/08 18:26] – 2015년 08월 09일 내용 추가 changwooproject:peekwindow [2015/08/08 20:22] (현재) – [설명] changwoo
줄 358: 줄 358:
  
 ===== 2015년 08월 09일 내용 추가 ===== ===== 2015년 08월 09일 내용 추가 =====
 +한 클리앙 회원분께서 이메일로 "리스트뷰 컨트롤 선택은 어떻게 하나요?"라는 질문을 보내 주셨습니다.
 +예전 파이썬을 막 시작할 때 코드를 잠시 보다가 개정도 할 겸, 답변도 드릴 겸 코드를 조금 수정하기로 맘 먹었습니다.
  
 +==== ListView Peek ====
 +타겟이 되는 리스트뷰에 적절히 원하는 이벤트를 날려주는 코드.
  
 +<code python listview_peek.py>
 +# -*- coding: utf-8 -*-
 +import win32gui
 +import win32api
 +import commctrl
 +import ctypes
 +from win32con import PAGE_READWRITE, MEM_COMMIT, MEM_RESERVE, MEM_RELEASE, PROCESS_ALL_ACCESS
 +
 +GetWindowThreadProcessId = ctypes.windll.user32.GetWindowThreadProcessId
 +VirtualAllocEx = ctypes.windll.kernel32.VirtualAllocEx
 +VirtualFreeEx = ctypes.windll.kernel32.VirtualFreeEx
 +OpenProcess = ctypes.windll.kernel32.OpenProcess
 +WriteProcessMemory = ctypes.windll.kernel32.WriteProcessMemory
 +ReadProcessMemory = ctypes.windll.kernel32.ReadProcessMemory
 +
 +
 +class LVItem(ctypes.Structure):
 +    """
 +    LVITEM structure: https://msdn.microsoft.com/en-us/library/windows/desktop/bb774760%28v=vs.85%29.aspx
 +
 +       UINT   mask
 +       int    iItem
 +       int    iSubItem
 +       UINT   state
 +       UINT   stateMask
 +       LPTSTR pszText
 +       int    cchTextMax
 +       int    iImage
 +       LPARAM lParam
 +
 +    Total 9 items
 +    """
 +    _fields_ = [
 +        ("mask", ctypes.c_uint),
 +        ("iItem", ctypes.c_int),
 +        ("iSubItem", ctypes.c_int),
 +        ("state", ctypes.c_uint),
 +        ("stateMask", ctypes.c_uint),
 +        ("pszText", ctypes.c_char_p),
 +        ("cchTextMax", ctypes.c_int),
 +        ("iImage", ctypes.c_int),
 +        ("lParam", ctypes.c_ulong),
 +    ]
 +
 +
 +class ListViewPeek(object):
 +
 +    def __init__(self, hwnd):
 +        self.hwnd = hwnd
 +        self.pid = ctypes.c_uint()
 +
 +        GetWindowThreadProcessId(self.hwnd, ctypes.addressof(self.pid))
 +        self.process_handle = OpenProcess(PROCESS_ALL_ACCESS, False, self.pid)
 +        self.p_lvi = VirtualAllocEx(self.process_handle, 0, 4096, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)
 +        self.p_buf = VirtualAllocEx(self.process_handle, 0, 4096, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)
 +
 +    def __del__(self):
 +        VirtualFreeEx(self.process_handle, self.p_buf, 0, MEM_RELEASE)
 +        VirtualFreeEx(self.process_handle, self.p_lvi, 0, MEM_RELEASE)
 +        win32api.CloseHandle(self.process_handle)
 +
 +    def get_item_count(self):
 +        return win32gui.SendMessage(self.hwnd, commctrl.LVM_GETITEMCOUNT)
 +
 +    def get_list_items(self):
 +        init_list = [0, 0, 0, 0, 0, self.p_buf, 4096, 0, 0]
 +        self.init_p_lvi(init_list)
 +
 +        num_items = self.get_item_count()
 +        items = []
 +        extraction_buffer = ctypes.create_string_buffer(4096)
 +
 +        for i in xrange(num_items):
 +            win32gui.SendMessage(self.hwnd, commctrl.LVM_GETITEMTEXT, i, self.p_lvi)
 +            self.__read_from_buffer(self.p_buf, ctypes.addressof(extraction_buffer), ctypes.sizeof(extraction_buffer))
 +            items.append(extraction_buffer.value)
 +        return items
 +
 +    def select_item(self, index, selected):
 +        """
 +        index:     zero-based item index. -1 for select all.
 +        selected:  set True/False to select/deselect.
 +        """
 +        lv_item_list = [
 +            commctrl.LVIF_STATE,
 +            0,
 +            0,
 +            commctrl.LVIS_SELECTED if selected else 0,
 +            commctrl.LVIS_SELECTED,
 +            0,
 +            0,
 +            0,
 +            0,
 +        ]
 +        self.init_p_lvi(lv_item_list)
 +        return win32gui.SendMessage(self.hwnd, commctrl.LVM_SETITEMSTATE, index, self.p_lvi)
 +
 +    def init_p_lvi(self, init_list):
 +        lv_item_buf = LVItem(*init_list)
 +        return self.__write_to_buffer(self.p_lvi, ctypes.byref(lv_item_buf), ctypes.sizeof(lv_item_buf))
 +
 +    def __write_to_buffer(self, source, destination, read_size):
 +        copied = ctypes.c_uint()
 +        p_copied = ctypes.addressof(copied)
 +        WriteProcessMemory(self.process_handle, source, destination, read_size, p_copied)
 +        return copied.value
 +
 +    def __read_from_buffer(self, source, destination, read_size):
 +        copied = ctypes.c_uint()
 +        p_copied = ctypes.addressof(copied)
 +        ReadProcessMemory(self.process_handle, source, destination, read_size, p_copied)
 +        return copied.value
 +
 +</code>
 +
 +=== 설명 ===
 +이전 코드의 약간 손보아 리스트 뷰에 대해 딱 필요한 기능을 꺼내 쓸 수 있도록 고쳤습니다. 그리고 이전에 생략한 설명을 첨부하도록 할께요. 어차피 혹시 또 코드를 볼 때가 있다면 다시 빨리 기억하기 편할 테니까요.
 +
 +우선 LVItem 클래스는 [[https://msdn.microsoft.com/ko-kr/library/windows/desktop/bb774760%28v=vs.85%29.aspx|LVITEM]] 구조체를 다시 정의한 것입니다. ctypes 레퍼런스를 참조하면 됩니다. 이전에는 'structure.pack('iiiiiiiii', .... )'이런 식으로 알아보기 어렵게 되었죠.
 +
 +ListViewPeek 클래스가 본 기능을 하는 녀석입니다. 생성 인자로 리스트뷰 컨트롤의 핸들을 넘겨 주도록 되어 있죠. 생성자에서는 [[https://msdn.microsoft.com/ko-kr/library/windows/desktop/ms633522%28v=vs.85%29.aspx|핸들의 PID를 얻고]], 해당 PID로부터 [[https://msdn.microsoft.com/ko-kr/library/windows/desktop/ms684320%28v=vs.85%29.aspx|프로세스 핸들을 얻어냅니다]].
 +
 +[[https://msdn.microsoft.com/en-us/library/windows/desktop/aa366890%28v=vs.85%29.aspx|VirtualAllocEx]]는 해당 프로세스에서 메모리 버퍼를 만듭니다. p_lvi는 LVITEM 구조체를 위한 영역으로 만들었고, p_buf는 LVITEM의 멤버 변수인 pszText를 위한 영역입니다. 사실 p_lvi가 4KiB까지 차지할 필요는 없지만, 그냥 놔 뒀습니다.
 +
 +리스트의 각 아이템 텍스트를 가져오는 방법은 이렇습니다. [[http://docs.activestate.com/activepython/3.3/pywin32/win32gui__SendMessage_meth.html|win32gui.SendMessage]]로 [[https://msdn.microsoft.com/en-us/library/windows/desktop/bb761055%28v=vs.85%29.aspx|LVM_GETITEMTEXT]] 이벤트를 던지면 됩니다. 이 때 lParam으로 던지는 LVITEM은 pszText, cchTextMax만 신경 쓰면 됩니다. 단, 데이터를 가져올 때 메모리에 마구 접근하면 안되므로 우리가 VirtualAllocEx를 사용해 별도로 마련한 버퍼에 프로그램이 가진 데이터를 야금야금 복사해 가면서 파이썬 코드로 불러들여야 합니다.
 +
 +우선 제대로 이벤트를 던지면 p_buf에는 각 항목의 텍스트가 복사됩니다. WinAPI가 여기까지는 해 줍니다. 그 다음
 +''<nowiki>__read_from_buffer()</nowiki>'' 메소드가 호출됩니다. 여기서 [[https://msdn.microsoft.com/ko-kr/library/windows/desktop/ms680553%28v=vs.85%29.aspx|ReadProcessMemory()]] 함수를 이용해 p_buf의 메모리를 다시 파이썬 스크립트 프로세스 쪽의 메모리 공간, extraction_buffer로 재차 복사합니다. 다른 프로세스에서 데이터를 끌어 오는 거라 좀 번거롭죠. extraction_buffer의 값을 드디어 파이썬 리스트가 받아 저장합니다.
 +
 +리스트의 각 항목을 선택하려면 [[https://msdn.microsoft.com/ko-kr/library/windows/desktop/bb761196%28v=vs.85%29.aspx|LVM_SETITEMSTATE]] 이벤트를 던져 주어야 합니다. 이 때 LVITEM 구조체는 이렇게 되어 있어야 합니다.
 +
 +  * mask: state를 변경하는 것이므로 ''LVIF_STATE'' 플래그를 줍니다.
 +  * state: [[https://msdn.microsoft.com/ko-kr/library/windows/desktop/bb774733%28v=vs.85%29.aspx|LVIS_SELECTED]] 플래그를 주면 해당 항목이 선택된 상태가 됩니다. 0이면 선택되지 않습니다.
 +  * stateMask: ''LVIS_SELECTED'' 플래그에 대해 검사를 할 것이므로 여기도 ''LVIS_SELECTED'' 플래그를 줍니다.
 +  * 이외: 이외의 필드는 0으로 해도 무방합니다.
 +
 +그런데 TeraCopy는 ListView Control에서 LVM_SETITEMSTATE 이벤트를 제대로 수신하지 않습니다. ListView Control을 이용한 다른 앱을 이용해서 확인하기 바랍니다. 추정컨데 TeraCopy는 [[https://msdn.microsoft.com/en-us/library/windows/desktop/bb761849%28v=vs.85%29.aspx|WM_CTL_COLORBTN]] 이벤트로 항목을 하이라이트하는 것 같습니다. 그래서 저는 예전에 제가 만들어 둔 [[https://www.dropbox.com/s/4ff6q0yx5zdciyn/SubRenamer.exe?dl=0|SubRenamer]]를 이용하였습니다.
 +
 +==== Control Picker ====
 +ListView Control를 찾아내고 조정하는 역할을 합니다.
 +
 +<code python control_picker.py>
 +# -*- coding: utf-8
 +import win32gui
 +import pywintypes
 +from listview_peek import ListViewPeek
 +
 +
 +class ControlPicker:
 +    def __init__(self, parent_window_name):
 +        self.parent_window_handle = 0
 +        self.child_windows = []
 +
 +        try:
 +            win32gui.EnumWindows(self.__enum_window_handler, parent_window_name)
 +        except pywintypes.error as e:
 +            if e[0] == 0:
 +                pass
 +
 +        win32gui.EnumChildWindows(self.parent_window_handle, self.__enum_child_window_handler, None)
 +
 +    def __enum_window_handler(self, window_handle, extra):
 +        window_text = win32gui.GetWindowText(window_handle)
 +        if window_text.find(extra) != -1:
 +            self.parent_window_handle = window_handle
 +            return pywintypes.FALSE  # stop enumerating
 +
 +    def __enum_child_window_handler(self, window_handle, extra):
 +        self.child_windows.append(window_handle)
 +
 +    def pick_control(self, target_class_name, target_control_id = None):
 +        for child_window in self.child_windows:
 +            window_class = win32gui.GetClassName(child_window)
 +            control_id = win32gui.GetDlgCtrlID(child_window)
 +
 +            if window_class != target_class_name:
 +                continue
 +
 +            if target_control_id:
 +                if target_control_id == control_id:
 +                    return child_window
 +            else:
 +                return child_window
 +
 +
 +if __name__ == '__main__':
 +    import sys
 +    import time
 +
 +    picker = ControlPicker('SubRenamer')
 +    list_view_control = picker.pick_control('SysListView32', 0x3EC)
 +    if not list_view_control:
 +        print "SysListView32 not found!"
 +        sys.exit(1)
 +
 +    peek = ListViewPeek(list_view_control)
 +
 +    print "There are %d items in the ListView control!" % (peek.get_item_count(), )
 +    for x in peek.get_list_items():
 +        print x
 +    print
 +
 +    if peek.get_item_count() < 2:
 +        print "Make sure Subrenamer has enough unmatched video lists!"
 +        sys.exit(1)
 +
 +    pause = 5
 +    win32gui.SetActiveWindow(picker.parent_window_handle)
 +    win32gui.SetForegroundWindow(list_view_control)
 +    print "Target window is selected. %d seconds sleep..." % (pause, )
 +    time.sleep(pause)
 +
 +    peek.select_item(-1, False)
 +    print "No item selected. %d seconds sleep..." % (pause, )
 +    time.sleep(pause)
 +
 +    peek.select_item(0, True)
 +    print "The first item selected. %d seconds sleep..." % (pause, )
 +    time.sleep(pause)
 +
 +    peek.select_item(-1, False)
 +    peek.select_item(1, True)
 +    print "The second item selected. %d seconds sleep..." % (pause, )
 +    time.sleep(pause)
 +
 +    peek.select_item(-1, True)
 +    print "All selected. %d seconds sleep..." % (pause, )
 +    time.sleep(pause)
 +
 +    peek.select_item(-1, False)
 +    print "None selected again."
 +    print "Done!"
 +
 +</code>
 +
 +=== 설명 ===
 +
 +{{:project:subrenamer.png?nolink&600|}}
 +
 +위 그림이 SubRenamer입니다. [[https://www.wxwidgets.org/|wxWidget]]을 이용하 C++ 버전으로 제작한 것인데 바이너리만 남고 소스가 어딨는지... 알 수 없네요. m( 보다시피 두 개의 ListView 컨트롤이 있습니다. 위쪽의 ID는 0x3EC, 아래쪽이 0x3ED입니다. Spy++로 확인 가능합니다.
 +
 +이 프로그램은 동영상과 자막을 맞춰주는 프로그램입니다. [[.:pysubrenamer|파이썬 버전으로도 문서를 만들었으니]] 참고하기 바랍니다. 이 프로그램은 간단하게 폴더 안에 같은 이름으로 mp4, avi, mkv 같은 동영상 확장자와 smi 확장자 한 쌍이 맞춰져 있는지를 확인합니다. 짝이 맞지 않으면 동영상은 위쪽 리스트에, 자막은 아래쪽 리스트에 표시됩니다. 실제 동영상이 아니어도 괜찮습니다. 0바이트짜리 파일을 만들어 적당히 video_001.mp4, video_002.mp4 이런 식으로 가짜 동영상 파일만 서너개 만든 후, 아래쪽의 'scan' 버튼만 눌러 보면 바로 결과를 확인할 수 있습니다.
 +
 +{{:project:subrenamer_capture_01.png?nolink|}}
 +
 +control picker 스크립트도 subrenamer 프로그램을 상대로 제작했습니다. 스크립트를 실행하면 subrenamer 창을 찾고, 창을 가장 위로 올린 다음, 리스트 컨트롤의 항목을 선택하는 데모를 보여 줍니다.
  
project/peekwindow.1439058416.txt.gz · 마지막으로 수정됨: 2015/08/08 18:26 저자 changwoo

Donate Powered by PHP Valid HTML5 Valid CSS Driven by DokuWiki