Как нарисовать пустой прямоугольник на экране с помощью Python

Я не эксперт, и я пытаюсь показать на экране прямоугольник, который следует за движениями мыши из исходной точки, точно так же, как когда вы выбираете что-то в 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)

Как видите, код нарисует прямоугольник, а предыдущие останутся до обновления экрана.

Единственное решение, с которым я пришел, - это взять значения пикселей, которые я буду рисовать, прежде чем установить их в черный цвет, и перерисовывать их каждый раз, но это делает мой код довольно медленным. Есть ли простой способ обновить экран быстрее, чтобы предотвратить это?

...

Отредактировано с решением.

Как было предложено @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.

Это сработает только в том случае, если вы останетесь ниже и правее своего исходного положения, но это как раз то, что мне было нужно. Не сложно улучшить его, чтобы принять любую второстепенную позицию.


person José Chamorro    schedule 12.06.2020    source источник
comment
Кроме того, дружеское напоминание о том, что вы должны помечать свои вопросы как решенные, если они решают вашу проблему. Это предотвращает загромождение сайта нерешенными вопросами, и пользователи, оказавшиеся здесь в будущем, также могут быстрее найти ответы :)   -  person Torxed    schedule 12.06.2020


Ответы (3)


Чтобы обновить старую нарисованную область, вам нужно либо вызвать win32gui.UpdateWindow или что-то подобное для обновления вашего конкретного окна, но поскольку технически вы рисуете не на поверхности, а на всем мониторе. Вам нужно будет аннулировать всю область вашего монитора, чтобы сказать Windows перерисовать что-либо на нем (или я так понимаю).

И чтобы преодолеть медлительность, вместо использования циклов for для создания границы, которая потребует X циклов для итерации перед завершением прямоугольника, вы можете использовать win32ui.Rectangle, чтобы нарисовать его за один раз:

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 не принуждает перерисовку, он просто просит окна сделать это, затем окна уважать его или нет. 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, кроме этого сообщения: Маркировка вашей программы Python как Windows с поддержкой высокого разрешения

Или изменить масштаб отображения Windows на 100%:

100%

Это приводит к тому, что проблема позиционирования исчезает, возможно, примите это во внимание, запросив у ОС масштаб и компенсацию.


Единственной ссылкой, которую я смог найти на «очистку старых рисунков», был этот пост: -window-is-moved">содержимое win32 изменилось, но не показывает обновление, пока окно не будет перемещено с тегом c++ win winapi. Надеюсь, это убережет некоторых людей от поиска хорошего примера.

person Torxed    schedule 12.06.2020
comment
@JoséChamorro Я исправил проблему с обновлением, повторно отрендерив всю сцену. Это немного глючит с точки зрения мерцания, но это потому, что Windows не имеет аппаратного ускорения (насколько я знаю) в том смысле, что рабочий стол должен отображаться как можно быстрее. Поэтому необходимо выполнить некоторые оптимизации, чтобы получить безупречный опыт. - person Torxed; 12.06.2020
comment
Большое спасибо, ваш ответ действительно решил мой вопрос. Я бы только добавил, что, что довольно интересно, я решил проблему «глюка», адаптировав ваше предложение использовать win32gui.InvalidateRect к исходному коду, который я загрузил (проверьте исправление выше). Кажется, что дешевле просто установить цвет заданного набора точек, чем запрашивать определенный прямоугольник, и результат, я думаю, практически такой же, как и при вызове DrawFocusRect. Я думаю, это все еще не очень элегантное решение. Спасибо еще раз! - person José Chamorro; 12.06.2020
comment
@JoséChamorro Интересно, что это действительно работает. И я даже пробовал win32gui.LineTo, и они ужасно медленные. Я собираюсь немного поэкспериментировать с этим, но, вероятно, не буду обновлять здесь, так как это ни к чему не приведет. Но это довольно интересно :) Хорошая находка! - person Torxed; 12.06.2020
comment
@SaddamBinSyed Вы можете прочитать комментарии или ответ, так как там есть несколько вариантов. В конце концов, использование win32gui.SetPixel для создания контура и win32gui.InvalidateRect для очистки старых является самым быстрым. - person Torxed; 16.06.2020
comment
@Torxed, я только что обнаружил, что DrawFocusRect рисует контур. но я получаю высокое мерцание. Я тестирую ваш приведенный выше код. пожалуйста посоветуйте избавиться от мерцания - person SaddamBinSyed; 16.06.2020
comment
@Torxed в дополнение к вышесказанному, я также хочу изменить цвет границы прямоугольника. пожалуйста, порекомендуйте - person SaddamBinSyed; 16.06.2020
comment
@SaddamBinSyed Опять же, как уже было сказано. Мерцание возникает из-за того, что окна не предназначены для более высокой частоты обновления. Вместо этого используйте win32gui.InvalidateRect, чтобы очистить только ту область, которая вам нужна, в сочетании с win32gui.SetPixel в сообщении OP, чтобы установить цвет и создать границу. - person Torxed; 16.06.2020
comment
@Torxed Спасибо за быстрый ответ. Я пробовал цвет для прямоугольника, но не повезло. win32gui.SetPixel(dc, m[0], m[1], color), где цвет зеленый = -16744448. пожалуйста, порекомендуйте - person SaddamBinSyed; 16.06.2020
comment
@SaddamBinSyed Я добавил свой последний пример, после этого вы сами по себе. Этот пример использования в лучшем случае сложен. И чем больше вы будете спрашивать, тем сложнее будет становиться. Если у вас есть конкретная проблема, я думаю, будет лучше, если вы откроете свой собственный вопрос, а не перегружаете раздел комментариев. См. мою правку для прямоугольника цвета red, который пытается обновить окно, в котором вы рисуете, в конечном счете, окна должны выполнить этот запрос на перерисовку окна и то, как окно обрабатывает эти события. . - person Torxed; 16.06.2020
comment
@Torxed, большое спасибо за ваш ценный ответ. - person SaddamBinSyed; 16.06.2020
comment
@SaddamBinSyed Вы разместили свой вопрос? У меня есть код, который вам нужен, включая решение проблемы с цветом. Я хотел бы ответить на мой первый вопрос! (у) - person José Chamorro; 19.06.2020
comment
@Torxed Привет, не хочу, чтобы это продолжалось и продолжалось, но я только что обнаружил, что часть обновления не будет работать в полноэкранном режиме ... это кошмар! Чтобы быть более ясным и странным: я использую этот код, чтобы увидеть прямоугольник, который установит пределы снимка экрана (с pyatogui), который делается при нажатии на конечную точку прямоугольника, который я хочу определить. Итак, в полноэкранном режиме я никогда не увижу прямоугольник... но все промежуточные прямоугольники, которые я рисую (но не вижу!) пока перетаскиваю мышью, появляются на скриншоте... Удивительно! XD - person José Chamorro; 19.06.2020
comment
@JoséChamorro Так и будет, поскольку технически это часть реального рендеринга экрана, по сути, вы рисуете свое собственное окно / значок на экране. То, что мы здесь делаем, рисуя пиксели, эквивалентно созданию настольного приложения. Эта библиотека на самом деле предназначена именно для этого, создает окно и рисует в нем, а не рисует в других окнах (рабочий стол тоже является окном). Есть слои, например, курсор находится на одном слое/буфере, и вы можете попытаться узнать, как рисовать на отдельном слое. Хотя, наверное, сложно. - person Torxed; 19.06.2020
comment
@Torxed На самом деле я начал с попытки нарисовать другой слой (не уверен, что понимаю, что вы имеете в виду). Я использовал pygame с прозрачным экраном... проблема заключалась в том, что чертова pygame не получит положение курсора, когда он находится в прозрачных участках прозрачного экрана... я только что понял, что... это можно решить, просто используя win32, чтобы получить положение мыши ... Я буду пытаться, спасибо за вашу помощь и объяснения! - person José Chamorro; 19.06.2020
comment
@Torxed, я добавил решение с помощью pygame ниже. Я был бы признателен за любые комментарии к этому методу, а также за любую оптимизацию, которую вы можете найти. Возможно, вам уже было достаточно этого, хотя xD (y). - person José Chamorro; 19.06.2020
comment
@JoséChamorro, спасибо за ваш ответ. Я не публиковал новый вопрос, так как столкнулся с проблемой рендеринга (старый прямоугольник все еще отображается на моем экране) на моей рабочей станции. у вас есть решение и для этого? - person SaddamBinSyed; 21.06.2020

Если вы используете 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 здесь

person Rishabh Bohra    schedule 12.06.2020
comment
Спасибо за ответ, но я ищу способ рисовать поверх основного экрана, а не поверх нового - person José Chamorro; 12.06.2020

После использования решения @Torex некоторое время у меня возникли некоторые проблемы, в частности, прямоугольники, которые я рисую с помощью этого метода, не отображаются в полноэкранном режиме, и они остаются видимыми (как это ни парадоксально), если я получаю скриншот раздела, в котором они были draw, поэтому раздел обновления решения не работает в полноэкранном режиме.

Ниже, возможно, более сложное решение с некоторыми плюсами и минусами:

плюсы:

  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])))
person José Chamorro    schedule 19.06.2020