Продолжаем работать с двумерными списками (таблицами).
В прошлом уроке была создана программа для удобной работы с таблицей ячеек:
from random import randrange as rnd, choice from tkinter import * import itertools root = Tk() root.geometry('800x600') canv = Canvas(root, bg = 'white') canv.pack(fill = BOTH, expand = 1) m = 34 # размер ячеек d = 2 # размер поля вокруг ячейки nr = 6 # количество строк nc = 8 # количество столбцов x0 = m // 2 # отступ от левого края y0 = m // 2 # отступ от вернего края colors = ['red','yellow','cyan','green'] class cell(): def __init__(self, r, c): # при создании указываем номер строки и столбца, в который помещаем self.n = rnd(10) # значение, с которым будем работать self.r = r # Номер сторки в двумерном списке. self.c = c # Номер столбца ... self.color = choice(colors) # случайный цвет из списка self.id = canv.create_rectangle(-100,0,-100,0,fill = self.color) # квадратик ячейки self.id_text = canv.create_text(-100,0, text = self.n, font = "Arial " + str(m//2)) # текст в квадратике, размер зависит от масштаба self.paint() def paint(self): x1 = x0 + self.c * m + d # рассчитать координаты левого верхнего угла. y1 = y0 + self.r * m + d # координаты правого нижнего угла. x2 = x1 + m - 2*d # - r y2 = y1 + m - 2*d canv.coords(self.id,x1,y1,x2,y2) canv.itemconfig(self.id,fill = self.color) # текст в центр ячейки x = (x1 + x2) / 2 y = (y1 + y2) / 2 canv.coords(self.id_text,x,y) canv.itemconfig(self.id_text, text = self.n) a = [] for r in range(nr): # 6 строк a.append([]) # создаем пустую строку for c in range(nc): # в каждой строке - 10 элементов a[r].append(cell(r,c)) # добавляем очередной элемент в строку def task(event): pass canv.bind('<3>', task) mainloop()
Рассмотрим некоторые задачи предыдущего урока.
Отметить всех соседей можно по-разному, например так:
def task(event): r = (event.y - y0) // m c = (event.x - x0) // m for dr,dc in [(0,0),(-1,0),(1,0),(0,-1),(0,1)]: # dr,dc - это смещение по строке и столбцу rr = dr + r cc = dc + c if rr in range(nr) and cc in range(nc): # Проверяем, является ли новый адрес допустимым a[rr][cc].color = 'orange' a[rr][cc].paint()
Если выделять соседей не только по сторонам, но и по дагоналям, то лучше использовать два цикла:
for dr in [-1,0,1]: for dc in [-1,0,1]:
Обратите внимание, чтобы в обоих случаях мы берем и саму клетку. Не всегда это нужно делать. Исключим ее из перебора:
for dr in [-1,0,1]: for dc in [-1,0,1]: if dr on dc:
if dr on dc
даст ложь только если оба значения равны нулю, т.е. на самой клетке, а истину — если хотя бы одно значение не равно нулю, т.е. на всех соседях.
В первом случае, чтобы убрать саму клетку из перебора, достаточно удалить (0,0) из цикла.
А еще можно использовать модуль itertools. Выделение всех соседей (8 штук):
def mark1(rr,cc): for dr,dc in itertools.product([-1,0,1],[-1,0,1]): r = dr + rr c = dc + cc if r in range(nr) and c in range(nc): a[r].color = 'orange' a[r].paint()
Выделение соседей и соседей соседей может показаться сложным. Да оно и будет сложным, если не использовать функцию. Вместо выделения соседей нужно вызвать функцию, которая выделяет соседей:
def mark(rr,cc): for dr,dc in [(-1,0),(1,0),(0,-1),(0,1)]: r = dr + rr c = dc + cc if r in range(nr) and c in range(nc): a[r].color = 'orange' a[r].paint() def task(event): for r in range(nr): for c in range(nc): a[r].color = 'gray' a[r].paint() rr = (event.y - y0) // m cc = (event.x - x0) // m for dr,dc in [(0,0),(-1,0),(1,0),(0,-1),(0,1)]: r = dr + rr c = dc + cc if r in range(nr) and c in range(nc): mark(r,c)
Перейдем к новым задачам. Рассмотрим удаление элементов, столбцов и строк.
Как вы помните из предыдущих уроков, удалять лучше с конца. Список двумерный, значит будет «два конца».
При удалении нужно понять, что делать с остальными ячейками: сдвигать или оставлять как есть. Для начала попробуем оставлять.
Добавим метод kill в класс cell
def kill(self): canv.delete(self.id) # Удалить с экрана квадратик canv.delete(self.id_text) # Удалить с экрана текст a[self.r].remove(a[self.r][self.c]) # Удалить из списка ячейку
Удалим ячейку:
def task(event): r = (event.y - y0) // m c = (event.x - x0) // m a[r].kill()
Работает?
По-моему, не очень работает. Точнее работает, но не всегда.
(Может помочь вывод значений ячеек в консоль. См. Справочник https://progras.ru/formatirovannyj-vyvod/
Удалить строку из списка еще проще, чем отдельную ячейку, но нельзя забывать, что нужно удалить изображения ячеек с экрана.
def task(event): r = (event.y - y0) // m #c = (event.x - x0) // m for c in range(nc-1,-1,-1): a[r].kill()
Почти работает.
«Почти», потому что кое-что остается. Посмотрите вывод в консоль и подумайте, что с этим можно сделать. Еще один маленький шаг.
def task(event): #r = (event.y - y0) // m c = (event.x - x0) // m for r in range(nr): a[r].kill()
И опять почти работает. Природа ошибки та же, что и в случае с удалением ячейки.
Проблема серьезная.
Если мы попробуем поменять значение элементов местами, то опять будут сложности.
Причина в том, что мы храним одну и ту же информацию в ДВУХ местах. И при изменении в одном месте, не меняем в другом. Мы нарушили очень важный принцип НЕдублирования информации.
О чем это я?
Смотрите внимательно: номер строки и номер столбца каждой ячейки мы храним в свойствах ячейки. Но при этом, положение ячейки в таблице (двумерном списке) так же определяется номером строки и номером столбца. Пока мы не перемещали ячейки, эта проблема себя никак не проявляла. Когда же мы взялись удалять ячейки или перемещать их, то возник конфликт между r,c записанными в ячейку и r,c, характеризующими позицию этой ячейки в массиве. В результате мы получили ячейки, которые находятся в таблице в одном месте, а рисуют себя так, как-будто находятся в другом.
r,c в ячейке нам нужны для того, чтобы знать, где рисовать квадратик (изображение), не более того. Нужно найти другой способ риования ячейки и убрать эту информацию из класса cell.
Предлагаю немного подумать, прежде, чем переходить к следующему уроку.