【问题标题】:How to draw an empty rectangle on screen with Python如何使用 Python 在屏幕上绘制一个空矩形
【发布时间】:2020-06-12 09:11:17
【问题描述】:

我不是专家,我想在屏幕上显示一个矩形,它会跟随鼠标从一个固定的起点移动,就像您在文字或绘画中选择某些东西一样。我附带了这段代码:

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)

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

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

【问题讨论】:

  • 另外,一个友好的提醒,如果您的问题解决了您的问题,您应该将它们标记为已解决。它使网站不会被“未解决”的问题弄得一团糟,并且将来在这里结束的用户也可能会更快地找到答案:)

标签: python drawing pywin32 updating win32gui


【解决方案1】:

为了刷新旧的绘制区域,您需要调用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 不会强制重绘,它只是要求 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 或 OpenCV 等框架之外,我还没有找到很多解决此问题的方法:Marking Your Python Program as High DPI Aware Seamlessly Windows

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

这会导致定位问题消失,或许可以通过向操作系统查询比例和补偿来考虑这一点。


关于“清除旧图纸”,我能找到的唯一参考是这篇文章:win32 content changed but doesn't show update unless window is moved 标记为c++ win winapi。希望这可以避免一些人在找到一个好的示例之前进行搜索。

【讨论】:

  • @JoséChamorro 我通过重新渲染整个场景解决了更新问题。就闪烁而言,这是一个“小故障”,但这是因为 Windows 不是硬件加速的(我知道),因为桌面应该尽可能快地呈现。因此,必须进行一些优化才能获得完美的体验。
  • 非常感谢,您的回答确实解决了我的问题。我只想补充一点,有趣的是,我通过将您使用 win32gui.InvalidateRect 的建议改编为我最初上传的代码解决了“故障”问题(检查上面的更正)。似乎只设置一组要求定义矩形的给定点的颜色更便宜,我认为结果与调用 DrawFocusRect 获得的结果几乎相同。我想这仍然不是一个非常优雅的解决方案。再次感谢!
  • @JoséChamorro 有趣的是,这确实有效。我什至尝试过win32gui.LineTo,它们的速度非常慢。我将继续玩这个,但可能不会在这里更新,因为它不会有任何贡献。但这很有趣 :) 很好的发现!
  • @JoséChamorro 它会,因为它在技术上是实际屏幕渲染的一部分,本质上你是在屏幕上绘制你自己的窗口/图标。因此,创建桌面应用程序相当于我们在此处绘制像素。这个库实际上就是为了做到这一点,创建一个窗口并在其中绘制,而不是在其他“窗口”上绘制(桌面也是一个窗口)。有图层,例如光标在一个图层/缓冲区上,您可以尝试找出如何在单独的图层上绘图。无论如何,可能很棘手。
  • @Torxed 我实际上是从尝试绘制不同的图层开始的(不确定我是否明白你的意思)。我使用带有透明屏幕的pygame...问题是当在透明屏幕的透明部分时,该死的pygame不会获得光标位置...我刚刚意识到...这可以通过使用来解决win32 获取鼠标位置...我会继续努力,感谢您的帮助和解释!
【解决方案2】:

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

【讨论】:

  • 感谢您的回答,但我正在寻找一种在主屏幕上绘制的方法,而不是在新屏幕上绘制
【解决方案3】:

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

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

优点:

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

  2. 它可以在任何屏幕模式下工作。

  3. 工作时不会出现故障

对比:

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

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

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

工作代码:

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])))

【讨论】:

    【解决方案4】:

    根据这里的一些答案,我已经为此工作了一段时间,这是我的解决方案。

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

    我想解决一些问题(一些轻微的闪烁和边框太细)所以如果有人知道这些我会很感激一些帮助(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)
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2022-01-20
      • 2014-01-25
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多