Прежде всего, это очень крутой проект, и добро пожаловать в SO!
Рефакторинг
Прежде чем двигаться дальше, чтобы добавить новую логику, стоит потратить время на очистку вашего текущего кода.
Списки и циклы
Прямо сейчас подход именования переменных как image0
, image1
... image12
очень жесткий и не масштабируемый. Если вам нужно добавить еще одно поле или значок приложения, вы в основном застряли в переписывании всего кода, чтобы учесть это изменение. Что касается масштабируемости, что если вы хотите 50, 100 или 1000 приложений? Это будет лот набора текста!
Вот почему списки и подобные им подобные массивы структуры были изобретены. Идея состоит в том, чтобы поместить в один контейнер одинаковые предметы. Вы можете зациклить над списком и сделать что-то для каждого элемента в списке. Я не собираюсь вдаваться в полное руководство по спискам и циклам, но они являются важными инструментами для любой задачи программирования, поэтому необходимо научиться использовать их, чтобы продвигаться вперед как программист.
В качестве конкретного примера в вашем непосредственном коде используйте одну переменную images = []
вместо image1
... image12
. Внутри фигурных скобок добавьте данные своего изображения, а затем получите доступ к ним с помощью images[n]
, где n
- индекс изображения, с которым вы хотите работать. Вы можете зациклить их такими конструкциями, как:
for image in images:
# do something with this image
Вы также можете использовать кортежи в качестве списков, которые нельзя изменить (я использую кортежи в этом приложении - аналогично спискам, но они выглядят как apps = ()
).
Словари
Хотя списки являются горизонтальными и хранят подобные элементы в коллекции, словари являются вертикальными или объединяют связанные, но различные свойства вместе в одну сущность. В вашем коде сущность "app" описывается несколькими строками и числами следующим образом:
{
"x": 125,
"y": 125,
"img": "np++.png",
"cmd": r"C:\Program Files (x86)\Notepad++\notepad++.exe"
}
Я составляю списки этих словарей для хранения данных о том, где я хочу, чтобы мои ящики и приложения отображались, и какие команды и изображения (или цвет / ширина в случае ящиков) связаны с каждым из них.
Установка
Наборы полезны для проверки членства. В этом приложении нам нужно определить, какие объекты холста являются значками приложения, а какие - выпадающими списками. Я использовал наборы для выполнения этой логики тегов с двумя непересекающимися наборами, содержащими идентификаторы для приложений и блоков соответственно.
Инкапсуляция
В настоящее время ваш код связан с логикой во многих местах. Класс получает доступ к большому количеству данных, содержащихся в глобальном состоянии. Это небезопасно: если вы что-то измените в глобальном состоянии, вы вполне можете вызвать ошибки или сломать класс. Попробуйте написать функции и классы с сильной инкапсуляцией и как можно меньшим количеством зависимостей между компонентами. В этом приложении можно аккуратно упаковать все в класс DragAndDrop
и просто передать параметры, чтобы сообщить ему, как работать. Таким образом, вызывающая сторона может взаимодействовать только с доступными общедоступными функциями класса, и сбои легко изолируются и предсказуемы.
Стиль очистки
По соглашению Python , используйте snake_case
для имен переменных и функций и UpperCamelCase
для классов. При публикации кода убедитесь, что отступы верны, поскольку Python использует отступы для определения того, в какой области блока находится каждая строка кода.
Помимо класса dnd
, который я переименовал в DragAndDrop
, имена ваших переменных понятны, что похвально!
Добавление нового поведения
После рефакторинга и настройки структур данных мы можем свободно добавлять новые функции.
Столкновение
Несмотря на то, что ваша функциональность перетаскивания прекрасна, еще нет кода, который бы определял, когда значок приложения перетаскивается на поле. Это немного сложнее: мы можем использовать canvas.find_overlapping()
для проверки перекрытия, но нам нужно убедиться, что значок выпал на поле, а не на другой значок. Когда движение остановится, мы можем вызвать эту функцию, чтобы сделать это:
def stop_movement(self, event):
self.__move = False
overlaps = self.canvas.find_overlapping(*self.canvas.bbox(self.movingimage))
if len(overlaps) > 1 and not self.movingimage[0] in self.__boxes and \
any(x in self.__boxes for x in overlaps):
subprocess.call(self.__apps[self.movingimage[0]])
Функция использует некоторые структуры данных, о которых я говорил ранее, для установления отношений между различными объектами.
Создание и уничтожение процессов
Используйте subprocess.call()
, чтобы сделать системный вызов, чтобы открыть новый процесс и заблокировать его до закрытия. Вы можете использовать subprocess.Popen()
, если хотите открыть несколько приложений без блокировки, чтобы дождаться их завершения. Проверьте документы для получения дополнительной информации. Я использовал словарь для сопоставления идентификаторов приложений с правильной командой для передачи в subprocess.Popen()
.
По вашему дополнительному запросу, вызовите kill()
в созданном подпроцессе, чтобы убить его Я сохраняю всю эту информацию в словаре self.__app
, но он может использовать рефакторинг для класса для правильной инкапсуляции, поскольку приложения приобретают свои собственные свойства и поведенческую логику.
Код
Обратите внимание, что это просто начальный рефакторинг с новым поведением; всегда есть возможности для улучшений, и некоторые из вариантов, которые я сделал с точки зрения организации данных, могут вам не понравиться, поэтому я рекомендую исследовать дальше и настроить по вкусу. Я также беспокоился только о добавлении двух приложений для текстового редактора, но вы можете добавить в кортеж apps
столько приложений, сколько захотите для дальнейшего тестирования.
import subprocess
from tkinter import *
class DragAndDrop:
def __init__(self, boxes, apps, width=1920, height=1080, bg="white"):
self.photos = []
self.__apps = {}
self.__boxes = set()
self.root = Tk()
self.canvas = Canvas(self.root, width=width, height=height, bg=bg)
self.canvas.pack()
for box in boxes:
self.__boxes.add(
self.canvas.create_rectangle(
box["x1"], box["y1"], box["x2"], box["y2"],
width=box["width"], fill=box["fill"]
)
)
for app in apps:
self.photos.append(PhotoImage(file=app["img"]))
self.__apps[(
self.canvas.create_image(app["x"], app["y"], image=self.photos[-1])
)] = {"cmd": app["cmd"], "running": False, "proc": None}
self.__move = False
self.canvas.bind("<Button-1>", self.start_movement)
self.canvas.bind("<ButtonRelease-1>", self.stop_movement)
self.canvas.bind("<Motion>", self.movement)
def run(self):
self.root.mainloop()
def start_movement(self, event):
self.initi_x = self.canvas.canvasx(event.x)
self.initi_y = self.canvas.canvasy(event.y)
self.movingimage = self.canvas.find_closest(
self.initi_x, self.initi_y, halo=5
)
if self.movingimage[0] in self.__apps:
self.__move = True
def stop_movement(self, event):
self.__move = False
overlaps = self.canvas.find_overlapping(*self.canvas.bbox(self.movingimage))
app = self.movingimage[0]
if len(overlaps) > 1 and app not in self.__boxes and not self.__apps[app]["running"] \
and any(x in self.__boxes for x in overlaps):
self.__apps[app]["proc"] = subprocess.Popen(self.__apps[app]["cmd"])
self.__apps[app]["running"] = True
elif app not in self.__boxes and self.__apps[app]["running"] \
and not any(x in self.__boxes for x in overlaps):
self.__apps[app]["proc"].kill()
self.__apps[app]["running"] = False
def movement(self, event):
if self.__move:
end_x = self.canvas.canvasx(event.x)
end_y = self.canvas.canvasy(event.y)
deltax = end_x - self.initi_x
deltay = end_y - self.initi_y
self.initi_x = end_x
self.initi_y = end_y
self.canvas.move(self.movingimage, deltax, deltay)
if __name__ == "__main__":
boxes = (
{"x1": 618, "y1": 100, "x2": 693, "y2": 175, "width": 5, "fill": "white"},
{"x1": 693, "y1": 100, "x2": 768, "y2": 175, "width": 5, "fill": "white"},
{"x1": 618, "y1": 175, "x2": 693, "y2": 250, "width": 5, "fill": "green"},
{"x1": 693, "y1": 175, "x2": 768, "y2": 250, "width": 5, "fill": "green"},
{"x1": 618, "y1": 250, "x2": 693, "y2": 325, "width": 5, "fill": "blue"},
{"x1": 693, "y1": 250, "x2": 768, "y2": 325, "width": 5, "fill": "blue"},
{"x1": 618, "y1": 325, "x2": 693, "y2": 400, "width": 5, "fill": "yellow"},
{"x1": 693, "y1": 325, "x2": 768, "y2": 400, "width": 5, "fill": "yellow"},
{"x1": 543, "y1": 175, "x2": 618, "y2": 250, "width": 5, "fill": "dark orange"},
{"x1": 468, "y1": 175, "x2": 543, "y2": 250, "width": 5, "fill": "dark orange"},
{"x1": 768, "y1": 175, "x2": 843, "y2": 250, "width": 5, "fill": "red"},
{"x1": 843, "y1": 175, "x2": 918, "y2": 250, "width": 5, "fill": "red"},
)
apps = (
{"x": 125, "y": 125, "img": "np++.png", "cmd": r"C:\Program Files (x86)\Notepad++\notepad++.exe"},
{"x": 125, "y": 225, "img": "vim.png", "cmd": r"C:\Program Files (x86)\Vim\vim74\vim.exe"},
)
dnd = DragAndDrop(boxes, apps)
dnd.run()
Демо
Вот быстрый запуск программы в Windows. Я открываю пару текстовых редакторов и проверяю коллизию.
![enter image description here](https://i.stack.imgur.com/bw2Lr.gif)