如何使用 Python 在屏幕上绘制一个空矩形

How to draw an empty rectangle on screen with Python

我不是专家,我正在尝试在屏幕上显示一个矩形,该矩形从一个确定的起点开始跟随鼠标移动,就像您 select 在文字或绘画中做某事一样。我带来了这段代码:

import win32gui
m=win32gui.GetCursorPos()
while True:
    n=win32gui.GetCursorPos()
    for i in range(n[0]-m[0]):
        win32gui.SetPixel(dc, m[0]+i, m[1], 0)
        win32gui.SetPixel(dc, m[0]+i, n[1], 0)
    for i in range(n[1]-m[1]):
        win32gui.SetPixel(dc, m[0], m[1]+i, 0)
        win32gui.SetPixel(dc, n[0], m[1]+i, 0)

如您所见,代码将绘制矩形,但之前的矩形将一直保留到屏幕更新。

我想到的唯一解决方案是在将像素值设置为黑色之前获取我要绘制的像素值,然后每次都重新绘制它们,但这使我的代码非常慢。有没有一种简单的方法可以更快地更新屏幕以防止这种情况发生?

...

用解决方案编辑。

按照@Torxed 的建议,使用win32gui.InvalidateRect 解决了更新问题。然而,我发现只设置我需要设置的点的颜色比要求一个矩形要便宜。第一个解决方案呈现得很干净,而第二个解决方案仍然有点小故障。最后,最适合我的代码是:

import win32gui

m=win32gui.GetCursorPos()
dc = win32gui.GetDC(0)

while True:
    n=win32gui.GetCursorPos()
    win32gui.InvalidateRect(hwnd, (m[0], m[1], GetSystemMetrics(0), GetSystemMetrics(1)), True)
    back=[]
    for i in range((n[0]-m[0])//4):
        win32gui.SetPixel(dc, m[0]+4*i, m[1], 0)
        win32gui.SetPixel(dc, m[0]+4*i, n[1], 0)
    for i in range((n[1]-m[1])//4):
        win32gui.SetPixel(dc, m[0], m[1]+4*i, 0)
        win32gui.SetPixel(dc, n[0], m[1]+4*i, 0)

四除法和四乘法是避免闪烁所必需的,但在视觉上与使用 DrawFocusRect 相同。

这只有在您从初始位置保持在下方和右侧时才有效,但这正是我所需要的。接受任何次要职位都不难。

如果你使用 OpenCV 就可以这样做

#draw either rectangles or circles by dragging the mouse like we do in Paint application

import cv2
import numpy as np

drawing = False # true if mouse is pressed
mode = True # if True, draw rectangle. Press 'm' to toggle to curve
ix,iy = -1,-1

#mouse callback function
def draw_circle(event,x,y,flags,param):
    global ix,iy,drawing,mode

    if event == cv2.EVENT_LBUTTONDOWN:
        drawing = True
        ix,iy = x,y

    elif event == cv2.EVENT_MOUSEMOVE:
        if drawing == True:
            if mode == True:
                cv2.rectangle(img,(ix,iy),(x,y),(0,255,0),-1)
            else:
                cv2.circle(img,(x,y),5,(0,0,255),-1)

    elif event == cv2.EVENT_LBUTTONUP:
        drawing = False
        if mode == True:
            cv2.rectangle(img,(ix,iy),(x,y),(0,255,0),-1)
        else:
            cv2.circle(img,(x,y),5,(0,0,255),-1)

#bind this mouse callback function to OpenCV window
img = np.zeros((512,512,3), np.uint8)
cv2.namedWindow('image')
cv2.setMouseCallback('image',draw_circle)

while(1):
    cv2.imshow('image',img)
    k = cv2.waitKey(1) & 0xFF
    if k == ord('m'):
        mode = not mode
    elif k == 27:
        break

cv2.destroyAllWindows()

本文摘自opencv官方文档here

为了刷新旧的绘制区域,您需要调用 win32gui.UpdateWindow 或类似的东西来更新您的特定 window,但由于您在技术上不是在表面上绘制,而是整个显示器。您需要使监视器的整个区域无效,以便告诉 windows 在其上重新绘制任何内容 (或者据我所知).

为了克服速度缓慢的问题,您可以使用 win32ui.Rectangle 一次性绘制它,而不是使用 for 循环创建边界,在完成矩形之前需要 X 次循环来迭代:

import win32gui, win32ui
from win32api import GetSystemMetrics

dc = win32gui.GetDC(0)
dcObj = win32ui.CreateDCFromHandle(dc)
hwnd = win32gui.WindowFromPoint((0,0))
monitor = (0, 0, GetSystemMetrics(0), GetSystemMetrics(1))

while True:
    m = win32gui.GetCursorPos()
    dcObj.Rectangle((m[0], m[1], m[0]+30, m[1]+30))
    win32gui.InvalidateRect(hwnd, monitor, True) # Refresh the entire monitor

这里可以做进一步的优化,比如不更新整个显示器,只更新你画过的部分等等。但这是基本概念:)

要创建一个没有填充的矩形,例如,您可以将 Rectangle 换成 DrawFocusRect。或者为了获得更多控制,甚至可以使用 win32gui.PatBlt

显然 setPixel 是最快的,所以这是我最后一个关于颜色和速度的例子,尽管它并不完美,因为 RedrawWindow 没有 force 重绘,它只是要求 windows 去做,然后由 windows 决定是否兑现。 InvalidateRect 在性能上更好一些,因为它要求事件处理程序在有空闲时间时清除矩形。但我还没有找到比 RedrawWindow 更具侵略性的方法,即使那样仍然很温和。例如,隐藏桌面图标,下面的代码将不起作用。

import win32gui, win32ui, win32api, win32con
from win32api import GetSystemMetrics

dc = win32gui.GetDC(0)
dcObj = win32ui.CreateDCFromHandle(dc)
hwnd = win32gui.WindowFromPoint((0,0))
monitor = (0, 0, GetSystemMetrics(0), GetSystemMetrics(1))

red = win32api.RGB(255, 0, 0) # Red

past_coordinates = monitor
while True:
    m = win32gui.GetCursorPos()

    rect = win32gui.CreateRoundRectRgn(*past_coordinates, 2 , 2)
    win32gui.RedrawWindow(hwnd, past_coordinates, rect, win32con.RDW_INVALIDATE)

    for x in range(10):
        win32gui.SetPixel(dc, m[0]+x, m[1], red)
        win32gui.SetPixel(dc, m[0]+x, m[1]+10, red)
        for y in range(10):
            win32gui.SetPixel(dc, m[0], m[1]+y, red)
            win32gui.SetPixel(dc, m[0]+10, m[1]+y, red)

    past_coordinates = (m[0]-20, m[1]-20, m[0]+20, m[1]+20)

位置和分辨率有问题?请注意,高 DPI 系统往往会导致一系列问题。除了转向 OpenGL 解决方案或使用诸如 wxPython or OpenCV other than this post: Marking Your Python Program as High DPI Aware Seamlessly Windows

之类的框架之外,我还没有找到很多解决方法

或将 Windows 显示比例更改为 100%:

这会导致定位问题消失,也许可以通过查询 OS 的比例和补偿来解决这个问题。


我能在 "clearing the old drawings" 上找到的唯一参考资料是这个 post:win32 content changed but doesn't show update unless window is moved 标记为 c++ win winapi。希望这可以避免一些人在找到好的例子之前进行搜索。

在使用@Torex 解决方案一段时间后,我遇到了一些问题,特别是,我用该方法绘制的矩形在全屏模式下不显示,如果我得到该部分的屏幕截图,它们仍然可见(矛盾的是)他们正在其中绘制,因此解决方案的更新部分在全屏模式下不起作用。

下面是一个可能更复杂的解决方案,有一些优点和缺点:

优点:

  1. 它允许使用 pygame 功能轻松地为绘制矩形的基本思想添加功能,包括轻松更改其颜色、宽度等。

  2. 它适用于任何屏幕模式并且。

  3. 工作时不会出现故障

对比:

  1. 它确实在开始和结束时出现故障,因为 pygame.display() 被调用并被杀死。

  2. 需要创建一个独立的window。

  3. 您需要对其进行调整以防止点击事件将您带出您要创建的透明window。

工作代码:

import win32api
from win32api import GetSystemMetrics
import win32con
import pygame
import win32gui
import pyautogui

pygame.init()
screen = pygame.display.set_mode((GetSystemMetrics(0), GetSystemMetrics(1)), pygame.FULLSCREEN, pygame.NOFRAME) # For borderless, use pygame.NOFRAME
done = False
fuchsia = (255, 0, 128)  # Transparency color
dark_red = (139, 0, 0)

# Set window transparency color
hwnd = pygame.display.get_wm_info()["window"]
win32gui.SetWindowLong(hwnd, win32con.GWL_EXSTYLE,
                       win32gui.GetWindowLong(hwnd, win32con.GWL_EXSTYLE) | win32con.WS_EX_LAYERED)
win32gui.SetLayeredWindowAttributes(hwnd, win32api.RGB(*fuchsia), 0, win32con.LWA_COLORKEY)

#Some controls
block=0
block1=0

#You can render some text
white=(255,255,255)
blue=(0,0,255)
font = pygame.font.Font('freesansbold.ttf', 32) 
texto=font.render('press "z" to define one corner and again to define the rectangle, it will take a screenshot', True, white, blue)
while not done:

    keys= pygame.key.get_pressed()
    pygame.time.delay(50)

    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            done = True

    #This controls the actions at starting and end point
    if block1==0:
        if keys[pygame.K_z]:
            if block==0:
                block=1
                n=win32gui.GetCursorPos()
            else:
                done=True
                break
            #this prevents double checks, can be handle also by using events
            block1=10

        else:
            m=win32gui.GetCursorPos()
    else:
        block1-=1        

    screen.fill(fuchsia)  # Transparent background
    #this will render some text
    screen.blit(texto,(0,0))
    #This will draw your rectangle
    if block==1:
        pygame.draw.line(screen,dark_red,(n[0],n[1]),(m[0],n[1]),1)
        pygame.draw.line(screen,dark_red,(n[0],m[1]),(m[0],m[1]),1)
        pygame.draw.line(screen,dark_red,(n[0],n[1]),(n[0],m[1]),1)
        pygame.draw.line(screen,dark_red,(m[0],n[1]),(m[0],m[1]),1)
        #    Drawing the independent lines is still a little faster than drawing a rectangle
        pygame.draw.rect(screen,dark_red,(min(n[0],m[0]),min(n[1],m[1]),abs(m[0]-n[0]),abs(m[1]-n[1])),1)
    pygame.display.update()    

pygame.display.quit()
pyautogui.screenshot(region=(min(n[0],m[0]),min(n[1],m[1]),abs(m[0]-n[0]),abs(m[1]-n[1])))

根据此处的一些答案,我已经研究了一段时间,这是我的解决方案。

这会等待鼠标被按下并拖动,并创建一个从按下位置到拖动位置的矩形。在鼠标释放时,它将清除矩形并输出点击和释放的位置并关闭所有挂钩。

有几个问题我想解决(一些轻微的闪烁和边框太细)所以如果有人知道这些我会很感激一些帮助(win32ui 文档真的很糟糕)

如果您想要纯色,只需将 FrameRect((x,y,a,b),brush) 更改为 FillRect((x,y,a,b), brush)

from win32gui import GetDC, WindowFromPoint, SetPixel, InvalidateRect
from win32ui import CreateDCFromHandle, CreateBrush
from win32api import GetSystemMetrics, GetSysColor
from PyHook3 import HookManager
import ctypes
class Draw_Screen_Rect:
    def __init__(self):
        self.pos = [0, 0, 0, 0]
        dc = GetDC(0)
        self.dcObj = CreateDCFromHandle(dc)
        self.hwnd = WindowFromPoint((0,0))
        self.monitor = (0, 0, GetSystemMetrics(0), GetSystemMetrics(1))
        self.clicked = False
        self.b1 = CreateBrush()
        self.b1.CreateSolidBrush(GetSysColor(255))
        self.final_rect = None
        self.refresh_frames = 0
        self.refresh_after = 10

    def _draw_rect_func(self):
        self.dcObj.FrameRect(tuple(self.pos), self.b1)
    def _refresh_rect(self):
        InvalidateRect(self.hwnd, self.monitor, True)
    

    def _OnMouseEvent(self, event):
        if event.Message == 513:
            self.clicked = True
            self.pos[0], self.pos[1] = event.Position
        elif event.Message == 514:
            self.clicked = False
            self.pos[2], self.pos[3] = event.Position
            self._draw_rect_func()
            self._refresh_rect()
            self.final_rect = self.pos
            self._destroy_hooks()
        elif event.Message == 512:
            if self.clicked:
                self.pos[2], self.pos[3] = event.Position
                if self.refresh_frames%2 ==0:
                    self._draw_rect_func()
                self.refresh_frames+=1
                if self.refresh_frames > self.refresh_after:
                    self.refresh_frames = 0
                    self._refresh_rect()
        return True
    def create_hooks(self):
        self.hm = HookManager()
        self.hm.MouseLeftDown = self._OnMouseEvent
        self.hm.MouseLeftUp = self._OnMouseEvent
        self.hm.MouseMove = self._OnMouseEvent
        self.hm.HookMouse()
        self.hm.HookKeyboard()

    def _destroy_hooks(self):
        self.hm.UnhookMouse()
        ctypes.windll.user32.PostQuitMessage(0)

    def output(self):
        return self.final_rect

if __name__ == '__main__':
    app = Draw_Screen_Rect()
    app.create_hooks()
    from pythoncom import PumpMessages
    PumpMessages()
    out = app.output()
    print(out)