如何用Python在屏幕上画一个空矩形

4 人关注

我不是一个专家,我试图在屏幕上显示一个矩形,这个矩形从一个固定的起点跟随鼠标移动,就像你在word或paint中选择东西一样。我想到了这个代码。

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)

正如你所看到的,代码将画出矩形,但之前的矩形将保持到屏幕更新。

我想到的唯一解决办法是,在将像素值设置为黑色之前,先将它们画出来,然后每次都重画,但这使我的代码很慢。有什么简单的方法可以更快地更新屏幕以防止这种情况吗?

Edited with solution.

正如@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是一样的。

这只有在你保持在初始位置的下方和右侧时才会起作用,但这正是我需要的。要改进它以接受任何次要位置并不困难。

1 个评论
另外,友好地提醒你,如果你的问题解决了,你应该把你的问题标记为已解决。这可以使网站不被 "未解决 "的问题所干扰,而且将来在这里结束的用户也可能更快地找到答案 :)
python
drawing
pywin32
updating
win32gui
José Chamorro
José Chamorro
发布于 2020-06-12
4 个回答
Torxed
Torxed
发布于 2021-09-11
已采纳
0 人赞同

为了刷新旧的绘制区域,你需要调用 win32gui.UpdateWindow 或类似的方法来更新你的特定窗口,但由于你在技术上不是在一个表面上绘图,而是在整个显示器上。你需要使你的显示器的整个区域失效,以便告诉窗口在上面重新绘制任何东西 (或我所理解的那样) .

为了克服速度慢的问题,你可以使用 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决定是否执行。替换代码6】在性能上要好一些,因为它要求事件处理程序在有空闲时间时清除矩形。但我还没有找到比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或OpenCV,而不是这个帖子。将你的Python程序标记为高DPI识别的无缝Windows

或者将Windows的显示比例改为100%

这导致了定位问题的消失,也许通过查询操作系统的比例来考虑这个问题,并进行补偿。

我能够找到的关于 "清除旧图纸 "的唯一参考资料是这个帖子。win32内容已经改变,但不显示更新,除非窗口被移动标签 c++winapi。希望这能让一些人在找到一个好的例子之前省去搜索的麻烦。

@JoséChamorro 我通过重新渲染整个场景解决了更新问题。在闪烁方面有点 "小毛病",但这是因为windows没有硬件加速(据我所知),也就是说桌面应该尽可能快地被渲染。所以必须进行一些优化,以获得完美的体验。
非常感谢你,你的回答确实解决了我的问题。我只想补充一点,有趣的是,我通过将你的建议--使用win32gui.InvalidateRect--改编成我最初上传的代码,解决了这个 "故障 "问题(请看上面的更正)。似乎只设置一组给定的点的颜色比要求一个定义的矩形更便宜,而且我认为结果与调用DrawFocusRect得到的结果几乎一样。我想这仍然是一个不太优雅的解决方案。再次感谢您!
@JoséChamorro 有趣的是,这确实有效。而且我甚至试过 win32gui.LineTo ,它们的速度慢得可怕。我打算继续玩一会儿,但可能不会在这里更新,因为这不会有什么贡献。但这是很有趣的 :)好的发现!
@JoséChamorro 会的,因为它在技术上是实际屏幕渲染的一部分,本质上你在屏幕上画你自己的窗口/图标。所以它相当于创建一个桌面应用程序,我们在这里做的是绘制像素。这个库实际上就是用来做这个的,创建一个窗口并在其中绘图,而不是在其他 "窗口 "上绘图(桌面也是一个窗口)。有一些层,例如光标是在一个层/缓冲区上,你可以试着找出如何在一个单独的层上绘图。不过,这可能很棘手。
@Torxed 其实我一开始是想在不同的层上画画(不知道我是否明白你的意思)。我在透明的屏幕上使用了pygame......问题是,在透明屏幕的透明部分,该死的pygame不会得到光标的位置。我刚刚意识到......这可以通过使用win32获得鼠标的位置来解决。我会继续尝试的,谢谢你的帮助和解释
Rishabh Bohra
Rishabh Bohra
发布于 2021-09-11
0 人赞同

如果你使用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

谢谢你的回答,但我正在寻找一种在主屏幕上绘图的方法,而不是在一个新的屏幕上。
José Chamorro
José Chamorro
发布于 2021-09-11
0 人赞同

在使用@Torex解决方案一段时间后,我遇到了一些问题,特别是我用该方法绘制的矩形在全屏模式下不显示,如果我对绘制矩形的部分进行截图,它们仍然可见(矛盾的是),所以该解决方案的更新部分在全屏模式下无法工作。

下面是一个也许更复杂的解决方案,有一些优点和缺点。

pros:

  • 它能够使用pygame的功能,轻松地在绘制矩形的基本概念上添加功能,包括轻松地改变其颜色、宽度等。

  • 它可以在任何屏幕模式下工作,并且。

  • it doesn't glitch while working

  • 在开始和结束时,当调用pygame.display()并将其杀死时,确实会出现故障。

  • 它需要创建一个独立的窗口。

  • 你将需要调整它,以防止点击事件将你带出你要创建的透明窗口。

  • The working code:

    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])))
        
    user3310078
    user3310078
    发布于 2021-09-11
    0 人赞同

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

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

    有几个问题我想解决(一些轻微的闪烁和边界太薄),所以如果有人知道这些,我将感谢一些帮助(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()