Wingnet v2.1.1

Wingnet 是一個視窗對齊小工具,透過 Claude 及 Gemini 協作完成,程式碼無需授權即可自由轉載、使用與改寫,不必另行告知。
程式的圖標透過 python 程式碼產生,並排除了部份因韌體熱鍵綁定的HOOK問題。
Last updated: 2026/7/7

#!/usr/bin/env python3
#
# Built with the collaboration of Claude and Gemini.
#
# Copyright (c) 2026
# Released under the MIT License.
#

import ctypes
import ctypes.wintypes as wt
import sys
import threading

from PIL import Image, ImageDraw
import pystray

user32   = ctypes.windll.user32
kernel32 = ctypes.windll.kernel32
dwmapi   = ctypes.windll.dwmapi


class KBDLLHOOKSTRUCT(ctypes.Structure):
    _fields_ = [
        ('vkCode',      wt.DWORD),
        ('scanCode',    wt.DWORD),
        ('flags',       wt.DWORD),
        ('time',        wt.DWORD),
        ('dwExtraInfo', ctypes.c_ulong),
    ]


class MONITORINFO(ctypes.Structure):
    _fields_ = [
        ('cbSize',    wt.DWORD),
        ('rcMonitor', wt.RECT),
        ('rcWork',    wt.RECT),
        ('dwFlags',   wt.DWORD),
    ]


class WINDOWPLACEMENT(ctypes.Structure):
    _fields_ = [
        ('length',           wt.UINT),
        ('flags',            wt.UINT),
        ('showCmd',          wt.UINT),
        ('ptMinPosition',    wt.POINT),
        ('ptMaxPosition',    wt.POINT),
        ('rcNormalPosition', wt.RECT),
    ]


WH_KEYBOARD_LL           = 13
WM_KEYDOWN               = 0x0100
WM_SYSKEYDOWN            = 0x0104
WM_QUIT                  = 0x0012
HC_ACTION                = 0

VK_CONTROL               = 0x11
VK_LWIN                  = 0x5B
VK_RWIN                  = 0x5C

SW_RESTORE               = 9
SW_MAXIMIZE              = 3
SW_SHOWMAXIMIZED         = 3
SWP_SHOWWINDOW           = 0x0040
SWP_NOZORDER             = 0x0004
MONITOR_DEFAULTTONEAREST = 0x00000002
HIGH_PRIORITY_CLASS      = 0x00000080
DWMWA_EXTENDED_FRAME_BOUNDS = 9

HOTKEY_MAP: dict[int, str] = {
    0x25: 'left_half',
    0x27: 'right_half',
    0x26: 'top_half',
    0x28: 'bottom_half',
    0x0D: 'maximize',
    ord('C'): 'center',
    ord('U'): 'top_left',
    ord('I'): 'top_right',
    ord('J'): 'bottom_left',
    ord('K'): 'bottom_right',
    ord('D'): 'left_third',
    ord('F'): 'center_third',
    ord('G'): 'right_third',
    ord('E'): 'left_twothirds',
    ord('T'): 'right_twothirds',

    0x67: 'move_top_left',
    0x24: 'move_top_left',
    0x61: 'move_bottom_left',
    0x23: 'move_bottom_left',
    0x69: 'move_top_right',
    0x21: 'move_top_right',
    0x63: 'move_bottom_right',
    0x22: 'move_bottom_right',
}


def get_work_area(hwnd: int) -> tuple[int, int, int, int]:
    monitor = user32.MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST)
    mi = MONITORINFO()
    mi.cbSize = ctypes.sizeof(MONITORINFO)
    user32.GetMonitorInfoW(monitor, ctypes.byref(mi))
    r = mi.rcWork
    return r.left, r.top, r.right - r.left, r.bottom - r.top


def get_shadow_insets(hwnd: int) -> tuple[int, int, int, int]:
    win_rect   = wt.RECT()
    frame_rect = wt.RECT()
    user32.GetWindowRect(hwnd, ctypes.byref(win_rect))
    hr = dwmapi.DwmGetWindowAttribute(
        hwnd,
        DWMWA_EXTENDED_FRAME_BOUNDS,
        ctypes.byref(frame_rect),
        ctypes.sizeof(wt.RECT),
    )
    if hr != 0:
        return 0, 0, 0, 0
    return (
        frame_rect.left   - win_rect.left,
        frame_rect.top    - win_rect.top,
        win_rect.right    - frame_rect.right,
        win_rect.bottom   - frame_rect.bottom,
    )


def place_window(hwnd: int, x: int, y: int, w: int, h: int, maximize: bool = False) -> None:
    if maximize:
        user32.ShowWindow(hwnd, SW_MAXIMIZE)
        return
    wp = WINDOWPLACEMENT()
    wp.length = ctypes.sizeof(WINDOWPLACEMENT)
    user32.GetWindowPlacement(hwnd, ctypes.byref(wp))
    if wp.showCmd == SW_SHOWMAXIMIZED:
        user32.ShowWindow(hwnd, SW_RESTORE)

    sl, st, sr, sb = get_shadow_insets(hwnd)
    user32.SetWindowPos(
        hwnd, 0,
        x - sl,
        y - st,
        w + sl + sr,
        h + st + sb,
        SWP_SHOWWINDOW | SWP_NOZORDER,
    )


def execute_action(action: str) -> None:
    hwnd = user32.GetForegroundWindow()
    if not hwnd:
        return
    try:
        ox, oy, ow, oh = get_work_area(hwnd)
    except Exception:
        return

    hw = ow // 2
    hh = oh // 2
    t1 = ow // 3
    t2 = ow * 2 // 3

    layout: dict[str, tuple[int, int, int, int]] = {
        'left_half':       (ox,          oy,         hw,          oh),
        'right_half':      (ox + hw,     oy,         ow - hw,     oh),
        'top_half':        (ox,          oy,         ow,          hh),
        'bottom_half':     (ox,          oy + hh,    ow,          oh - hh),
        'top_left':        (ox,          oy,         hw,          hh),
        'top_right':       (ox + hw,     oy,         ow - hw,     hh),
        'bottom_left':     (ox,          oy + hh,    hw,          oh - hh),
        'bottom_right':    (ox + hw,     oy + hh,    ow - hw,     oh - hh),
        'left_third':      (ox,          oy,         t1,          oh),
        'center_third':    (ox + t1,     oy,         t2 - t1,     oh),
        'right_third':     (ox + t2,     oy,         ow - t2,     oh),
        'left_twothirds':  (ox,          oy,         t2,          oh),
        'right_twothirds': (ox + t1,     oy,         ow - t1,     oh),
    }

    try:
        if action == 'maximize':
            place_window(hwnd, 0, 0, 0, 0, maximize=True)
        elif action in ('center', 'move_top_left', 'move_bottom_left', 'move_top_right', 'move_bottom_right'):
            win_rect = wt.RECT()
            user32.GetWindowRect(hwnd, ctypes.byref(win_rect))
            cur_w = win_rect.right - win_rect.left
            cur_h = win_rect.bottom - win_rect.top
            sl, st, sr, sb = get_shadow_insets(hwnd)
            vw = cur_w - sl - sr
            vh = cur_h - st - sb
            if action == 'center':
                cx = ox + (ow - vw) // 2
                cy = oy + (oh - vh) // 2
            elif action == 'move_top_left':
                cx = ox
                cy = oy
            elif action == 'move_bottom_left':
                cx = ox
                cy = oy + oh - vh
            elif action == 'move_top_right':
                cx = ox + ow - vw
                cy = oy
            elif action == 'move_bottom_right':
                cx = ox + ow - vw
                cy = oy + oh - vh
            place_window(hwnd, cx, cy, vw, vh)
        elif action in layout:
            place_window(hwnd, *layout[action])
    except Exception as exc:
        print(f'[Wingnet] place_window error: {exc}', file=sys.stderr)


LRESULT  = ctypes.c_longlong
_WPARAM  = ctypes.c_ulonglong
_LPARAM  = ctypes.c_longlong

_LowLevelKeyboardProc = ctypes.WINFUNCTYPE(
    LRESULT, ctypes.c_int, _WPARAM, _LPARAM
)

user32.CallNextHookEx.restype  = LRESULT
user32.CallNextHookEx.argtypes = [wt.HHOOK, ctypes.c_int, _WPARAM, _LPARAM]

_hook_handle:    int = 0
_hook_proc_ref       = None
_hook_thread_id: int = 0
_hook_ready = threading.Event()


def _keyboard_proc(nCode: int, wParam: int, lParam: int) -> int:
    if nCode == HC_ACTION and wParam in (WM_KEYDOWN, WM_SYSKEYDOWN):
        kb   = ctypes.cast(lParam, ctypes.POINTER(KBDLLHOOKSTRUCT)).contents
        vk   = kb.vkCode
        ctrl = bool(user32.GetAsyncKeyState(VK_CONTROL) & 0x8000)
        win_ = (bool(user32.GetAsyncKeyState(VK_LWIN) & 0x8000) or
                bool(user32.GetAsyncKeyState(VK_RWIN) & 0x8000))
        if ctrl and win_ and vk in HOTKEY_MAP:
            threading.Thread(
                target=execute_action,
                args=(HOTKEY_MAP[vk],),
                daemon=True,
            ).start()
            return 1
    return user32.CallNextHookEx(_hook_handle, nCode, wParam, lParam)


def _hook_thread_func() -> None:
    global _hook_handle, _hook_proc_ref, _hook_thread_id

    _hook_thread_id = kernel32.GetCurrentThreadId()
    _hook_proc_ref  = _LowLevelKeyboardProc(_keyboard_proc)
    _hook_handle    = user32.SetWindowsHookExW(
        WH_KEYBOARD_LL,
        _hook_proc_ref,
        None,
        0,
    )

    if not _hook_handle:
        err = kernel32.GetLastError()
        print(f'[Wingnet] SetWindowsHookExW 失敗,錯誤碼: {err}', file=sys.stderr)
        _hook_ready.set()
        return

    _hook_ready.set()

    msg = wt.MSG()
    while user32.GetMessageW(ctypes.byref(msg), None, 0, 0) > 0:
        user32.TranslateMessage(ctypes.byref(msg))
        user32.DispatchMessageW(ctypes.byref(msg))

    user32.UnhookWindowsHookEx(_hook_handle)


def stop_hook() -> None:
    if _hook_thread_id:
        user32.PostThreadMessageW(_hook_thread_id, WM_QUIT, 0, 0)


def _make_icon(size: int = 64) -> Image.Image:
    img  = Image.new('RGBA', (size, size), (0, 0, 0, 0))
    draw = ImageDraw.Draw(img)
    s, m = size - 1, size // 2
    c    = (80, 160, 255, 255)
    draw.rectangle([2, 2, s - 2, s - 2], outline=c, width=3)
    draw.line([m, 2, m, s - 2], fill=c, width=2)
    draw.line([2, m, s - 2, m], fill=c, width=2)
    draw.rectangle([4, 4, m - 2, m - 2], fill=(80, 160, 255, 100))
    return img


_MENU_ROWS = [
    ('Wingnet (v2.1.1)',               False),
    (None, None),
    ('── 半版 ─────────────────────',  False),
    ('Ctrl+Win+←    左半',            False),
    ('Ctrl+Win+→    右半',            False),
    ('Ctrl+Win+↑    上半',            False),
    ('Ctrl+Win+↓    下半',            False),
    ('── 最大化 / 置中 ─────────────', False),
    ('Ctrl+Win+Enter  最大化',          False),
    ('Ctrl+Win+C      置中 (不改變大小)', False),
    ('── 四角對齊 (不改變大小) ─────', False),
    ('Ctrl+Win+Num7  對齊左上',        False),
    ('Ctrl+Win+Num9  對齊右上',        False),
    ('Ctrl+Win+Num1  對齊左下',        False),
    ('Ctrl+Win+Num3  對齊右下',        False),
    ('── 四角 ─────────────────────',  False),
    ('Ctrl+Win+U    左上角',           False),
    ('Ctrl+Win+I    右上角',           False),
    ('Ctrl+Win+J    左下角',           False),
    ('Ctrl+Win+K    右下角',           False),
    ('── 三等分 ───────────────────',  False),
    ('Ctrl+Win+D    左 ⅓',              False),
    ('Ctrl+Win+F    中 ⅓',              False),
    ('Ctrl+Win+G    右 ⅓',              False),
    ('Ctrl+Win+E    左 ⅔',              False),
    ('Ctrl+Win+T    右 ⅔',              False),
    (None, None),
    ('結束 Wingnet', True),
]


def _on_quit(icon: pystray.Icon, _item) -> None:
    stop_hook()
    icon.stop()


def create_tray_icon() -> pystray.Icon:
    items = []
    for label, clickable in _MENU_ROWS:
        if label is None:
            items.append(pystray.Menu.SEPARATOR)
        elif clickable:
            items.append(pystray.MenuItem(label, _on_quit))
        else:
            items.append(pystray.MenuItem(label, None, enabled=False))
    return pystray.Icon('Wingnet', _make_icon(), 'Wingnet', pystray.Menu(*items))


def main() -> None:
    try:
        ctypes.windll.shcore.SetProcessDpiAwareness(2)
    except OSError:
        user32.SetProcessDPIAware()

    kernel32.SetPriorityClass(kernel32.GetCurrentProcess(), HIGH_PRIORITY_CLASS)

    hook_thread = threading.Thread(
        target=_hook_thread_func, daemon=True, name='Wingnet-Hook'
    )
    hook_thread.start()
    _hook_ready.wait(timeout=3)

    if not _hook_handle:
        print('[Wingnet] Hook 安裝失敗,程式結束。', file=sys.stderr)
        sys.exit(1)

    icon = create_tray_icon()
    icon.run()

    stop_hook()
    hook_thread.join(timeout=2)


if __name__ == '__main__':
    main()
Python
展開

Python WindowsApp

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *