Вот исходный код моего приложения:
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import pandas as pd
import re
from functools import partial

def extract_price(text):
    if not isinstance(text, str):
        return ""
    m = re.search(r'(\d{1,3}(?:\s?\d{3})*)\s*руб\.', str(text).replace('\xa0', ' '))
    return m.group(1).replace(' ', '') if m else ""

def process_row(clean_lines, checked_top, checked_bottom):
    k_top = min(sum(checked_top), len(clean_lines))
    k_bottom = min(sum(checked_bottom), max(0, len(clean_lines) - k_top))
    name = " ".join(clean_lines[:k_top]) if k_top > 0 else ""
    desc_lines = clean_lines[k_top:len(clean_lines)-k_bottom if k_bottom > 0 else None]
    desc = "\n".join(desc_lines)
    return name, desc

def clean_lines_from_text(desc_raw):
    lines = [re.sub(r'\s+', ' ', l.strip().replace('\xa0', ' ')) for l in str(desc_raw).splitlines() if l.strip()]
    return lines

def calculate_price_excl_vat(price_str):
    try:
        price_float = float(price_str.replace(' ', '').replace(',', '.'))
        price_wo_vat = round(price_float / 1.2, 2)
        return str(price_wo_vat).replace('.', ',')
    except Exception:
        return ""

class TableWithCheckboxes(tk.Frame):
    def __init__(self, master, headers, *args, **kwargs):
        super().__init__(master, *args, **kwargs)
        self.headers = headers
        self.rows_widgets = []

        # Уменьшены минимальные ширины, чтобы таблица помещалась в окно
        self.col_widths = [
            max(len(h), 12) if idx < 4 else max(len(h), 18)
            for idx, h in enumerate(headers)
        ]

        self.grid_rowconfigure(0, weight=1)
        self.grid_columnconfigure(0, weight=1)

        self.canvas = tk.Canvas(self, borderwidth=0, highlightthickness=0)
        self.v_scroll = ttk.Scrollbar(self, orient="vertical", command=self.canvas.yview)
        self.h_scroll = ttk.Scrollbar(self, orient="horizontal", command=self.canvas.xview)
        self.canvas.configure(yscrollcommand=self.v_scroll.set, xscrollcommand=self.h_scroll.set)

        self.canvas.grid(row=0, column=0, sticky='nsew')
        self.v_scroll.grid(row=0, column=1, sticky='ns')
        self.h_scroll.grid(row=1, column=0, sticky='ew')

        self.table_frame = tk.Frame(self.canvas)
        self.canvas_frame_id = self.canvas.create_window((0, 0), window=self.table_frame, anchor="nw")

        self.table_frame.bind("<Configure>", self._on_frame_configure)
        self.canvas.bind("<Configure>", self._on_canvas_configure)

        self.header_labels = []
        self._make_header()

        for i, width in enumerate(self.col_widths):
            self.table_frame.grid_columnconfigure(i, weight=1, minsize=width * 7)

        # Переменные для редактирования
        self.editing_widgets = dict()  # {(row_idx, col_idx): Entry}

    def _make_header(self):
        for lbl in self.header_labels:
            lbl.destroy()
        self.header_labels.clear()
        for i, hdr in enumerate(self.headers):
            wrap_len_px = self.col_widths[i] * 7
            lbl = tk.Label(self.table_frame, text=hdr, relief="ridge", borderwidth=2,
                           font=('Arial', 11, 'bold'), width=self.col_widths[i], height=2,
                           anchor="center", bg="#dde7f3", wraplength=wrap_len_px, justify='center')
            lbl.grid(row=0, column=i, sticky="nsew", padx=2, pady=2)
            self.header_labels.append(lbl)
            self.table_frame.grid_columnconfigure(i, weight=1, minsize=self.col_widths[i] * 7)

    def _on_frame_configure(self, event):
        self.canvas.configure(scrollregion=self.canvas.bbox("all"))

    def _on_canvas_configure(self, event=None):
        width = event.width if event else self.canvas.winfo_width()
        self.canvas.itemconfigure(self.canvas_frame_id, width=width)

    def clear_table(self):
        for widgets in self.rows_widgets:
            for w in widgets:
                w.destroy()
        self.rows_widgets.clear()
        self.canvas.yview_moveto(0)
        self.canvas.xview_moveto(0)
        self.editing_widgets.clear()

    def update_column_widths(self, texts, col_idx):
        current_max_len = self.col_widths[col_idx]
        max_text_len = max((len(str(t)) for t in texts), default=0)
        header_len = len(self.headers[col_idx])

        if col_idx == len(self.headers) - 1 or col_idx == len(self.headers) - 2:
            base_min_width = 18
        else:
            base_min_width = 12

        new_width = max(current_max_len, max_text_len, header_len, base_min_width)

        if new_width > self.col_widths[col_idx]:
            self.col_widths[col_idx] = new_width
            self._make_header()

            for row_widgets in self.rows_widgets:
                if row_widgets and len(row_widgets) > col_idx:
                    if col_idx <= 4:
                        row_widgets[col_idx].config(width=self.col_widths[col_idx])
                    elif col_idx >= 5:
                        row_widgets[col_idx].config(width=self.col_widths[col_idx] * 7)

            if self.canvas.winfo_exists() and self.canvas.winfo_width() > 0 and self.canvas.winfo_height() > 0:
                self._on_canvas_configure()

    def insert_row(self, row_idx, art, name, descr, price, price_wo_vat, vars_top, vars_bottom, callback):
        row = []

        desc_lines = descr.split('\n')
        n_lines = max(1, len(desc_lines))
        font = ('Arial', 10)
        bg = "#f9fafd" if row_idx % 2 == 0 else "#f2f6fb"

        # Столбец 0 - Артикул
        art_lbl = tk.Label(self.table_frame, text=art, relief='ridge', borderwidth=1,
                           width=self.col_widths[0], height=n_lines, font=font, anchor="w", justify='left', bg=bg)
        art_lbl.grid(row=row_idx + 1, column=0, sticky="nsew")
        art_lbl.bind('<Double-1>', partial(self._start_edit, row_idx, 0))
        row.append(art_lbl)

        # Столбец 1 - Наименование
        name_lbl = tk.Label(self.table_frame, text=name, relief='ridge', borderwidth=1,
                            wraplength=self.col_widths[1]*7, height=n_lines, font=font,
                            anchor="w", justify='left', bg=bg)
        name_lbl.grid(row=row_idx + 1, column=1, sticky="nsew")
        name_lbl.bind('<Double-1>', partial(self._start_edit, row_idx, 1))
        row.append(name_lbl)

        # Столбец 2 - Описание
        descr_lbl = tk.Label(self.table_frame, text=descr, relief='ridge', borderwidth=1,
                             width=self.col_widths[2], height=n_lines, font=font, anchor="w", justify='left', bg=bg)
        descr_lbl.grid(row=row_idx + 1, column=2, sticky="nsew")
        descr_lbl.bind('<Double-1>', partial(self._start_edit, row_idx, 2))
        row.append(descr_lbl)

        # Столбец 3 - Цена
        price_lbl = tk.Label(self.table_frame, text=price, relief='ridge', borderwidth=1,
                             width=self.col_widths[3], height=n_lines, font=font, anchor='center', justify='center', bg=bg)
        price_lbl.grid(row=row_idx + 1, column=3, sticky="nsew")
        price_lbl.bind('<Double-1>', partial(self._start_edit, row_idx, 3))
        row.append(price_lbl)

        # Столбец 4 - Цена без НДС
        price_wo_vat_lbl = tk.Label(self.table_frame, text=price_wo_vat, relief='ridge', borderwidth=1,
                                    width=self.col_widths[4], height=n_lines, font=font, anchor='center', justify='center', bg=bg)
        price_wo_vat_lbl.grid(row=row_idx + 1, column=4, sticky="nsew")
        price_wo_vat_lbl.bind('<Double-1>', partial(self._start_edit, row_idx, 4))
        row.append(price_wo_vat_lbl)

        # Чекбоксы - не редактируемые
        cb_frame_top = tk.Frame(self.table_frame, relief='ridge', borderwidth=1, bg=bg)
        cb_frame_top.grid(row=row_idx + 1, column=5, sticky="nsew", padx=2, pady=2)
        row.append(cb_frame_top)
        for i in range(3):
            cb = tk.Checkbutton(cb_frame_top, variable=vars_top[i], text=str(i + 1),
                                command=callback, bg=bg)
            cb.pack(side='left', padx=5, pady=n_lines * 6 // 3)

        cb_frame_bottom = tk.Frame(self.table_frame, relief='ridge', borderwidth=1, bg=bg)
        cb_frame_bottom.grid(row=row_idx + 1, column=6, sticky="nsew", padx=2, pady=2)
        row.append(cb_frame_bottom)
        for i in range(3):
            cb = tk.Checkbutton(cb_frame_bottom, variable=vars_bottom[i], text=str(i + 1),
                                command=callback, bg=bg)
            cb.pack(side='left', padx=5, pady=n_lines * 6 // 3)

        self.rows_widgets.append(row)

    def _start_edit(self, row_idx, col_idx, event):
        # Проверяем редактируется ли уже эта ячейка
        key = (row_idx, col_idx)
        if key in self.editing_widgets:
            return

        label = self.rows_widgets[row_idx][col_idx]
        # Получаем позицию label относительной таблице
        x = label.winfo_x()
        y = label.winfo_y()
        width = label.winfo_width()
        height = label.winfo_height()

        # Создаём Entry как поле редактирования поверх label 
        entry = tk.Text(self.table_frame, font=label.cget("font"),
                        wrap='word', undo=True)
        entry.place(x=x, y=y, width=width, height=height)

        # Вставляем текущий текст
        current_text = label.cget("text")
        entry.insert('1.0', current_text)
        entry.focus_set()

        # Сохраняем для контроля
        self.editing_widgets[key] = entry

        # Функции для сохранения/отмены при событиях
        def save_edit(event=None):
            new_text = entry.get('1.0', 'end-1c')
            label.config(text=new_text)
            entry.destroy()
            del self.editing_widgets[key]

            # Если редактируемые колонки влияют на данные (например, описание), то обновляем данные
            if hasattr(self.master, 'update_row'):
                self.master.update_row(row_idx)

        def cancel_edit(event=None):
            entry.destroy()
            if key in self.editing_widgets:
                del self.editing_widgets[key]

        # Обработка нажатий клавиш
        entry.bind("<FocusOut>", save_edit)
        entry.bind("<Return>", lambda e: (save_edit(), "break"))
        entry.bind("<Escape>", lambda e: (cancel_edit(), "break"))

        # Поддержка Ctrl-C, Ctrl-V, Ctrl-Z уже встроена в tk.Text по умолчанию

class DynamicTableApp:
    def __init__(self, master):
        self.master = master
        self.master.title("Milesight: Таблица с динамическими чекбоксами")

        # Разворачиваем окно на весь экран (работает на Windows)
        self.master.state('zoomed')

        self.df = None
        self.rowdata = []

        self.headers = [
            'Артикул', 'Наименование', 'Описание', 'РРЦ', 'РРЦ (без НДС)',
            'Удалить первые строки', 'Удалить последние строки'
        ]

        top_frame = ttk.Frame(master)
        top_frame.pack(fill='x', pady=5)

        ttk.Button(top_frame, text="Загрузить Excel", command=self.load_file).pack(side='left', padx=10)

        filter_frame = ttk.Frame(master)
        filter_frame.pack(fill='x', pady=5)
        ttk.Label(filter_frame, text="Текст для исключения строк (фильтр):").pack(side='left', padx=5)
        self.filter_text_var = tk.StringVar(value="Модель снята с производства")
        filter_entry = ttk.Entry(filter_frame, textvariable=self.filter_text_var, width=60)
        filter_entry.pack(side='left', padx=5, fill='x', expand=True)

        sel_frame = ttk.Frame(master)
        sel_frame.pack(fill='x', pady=5)

        ttk.Label(sel_frame, text="Артикул:").grid(row=0, column=0, sticky='e')
        self.art_var = tk.StringVar()
        self.art_cb = ttk.Combobox(sel_frame, textvariable=self.art_var, width=30)
        self.art_cb.grid(row=0, column=1, padx=5)

        ttk.Label(sel_frame, text="Описание (Text3):").grid(row=0, column=2, sticky='e')
        self.desc_var = tk.StringVar()
        self.desc_cb = ttk.Combobox(sel_frame, textvariable=self.desc_var, width=30)
        self.desc_cb.grid(row=0, column=3, padx=5)

        ttk.Button(sel_frame, text="Показать таблицу", command=self.process).grid(row=0, column=4, padx=10)

        self.save_btn = ttk.Button(sel_frame, text="Сохранить", command=self.save, state='disabled')
        self.save_btn.grid(row=0, column=5)

        self.table = TableWithCheckboxes(master, self.headers)
        self.table.pack(fill='both', expand=True, padx=10, pady=10)

    def load_file(self):
        filename = filedialog.askopenfilename(filetypes=[("Excel files", "*.xlsx"), ("All files", "*.*")])
        if not filename:
            return
        self.df = pd.read_excel(filename)
        cols = list(self.df.columns)
        self.art_cb['values'] = cols
        self.desc_cb['values'] = cols
        self.art_var.set('Text2' if 'Text2' in cols else (cols[0] if cols else ''))
        self.desc_var.set('Text3' if 'Text3' in cols else (cols[-1] if cols else ''))
        messagebox.showinfo("Готово", "Файл успешно загружен")

    def process(self):
        if self.df is None:
            messagebox.showerror("Ошибка", "Сначала загрузите файл")
            return

        filter_text = self.filter_text_var.get().strip()
        self.table.clear_table()
        self.table.col_widths = [
            max(len(h), 12) if idx < len(self.headers) - 2 else max(len(h), 18)
            for idx, h in enumerate(self.headers)
        ]
        self.table._make_header()
        self.rowdata.clear()

        art_col = self.art_var.get()
        desc_col = self.desc_var.get()

        idx_for_rowdata = 0

        for i, row in enumerate(self.df.itertuples()):
            desc_raw = getattr(row, desc_col)
            if filter_text and isinstance(desc_raw, str) and filter_text in desc_raw:
                continue
            art = getattr(row, art_col) if hasattr(row, art_col) else ""
            price_str = extract_price(desc_raw)
            price_wo_vat_str = calculate_price_excl_vat(price_str)
            lines = clean_lines_from_text(desc_raw)

            vars_top = [tk.IntVar(value=1 if j < 2 else 0) for j in range(3)]
            vars_bottom = [tk.IntVar(value=1) for _ in range(3)]

            callback = partial(self.update_row, idx_for_rowdata)
            for v in vars_top + vars_bottom:
                v.trace_add('write', callback)

            name, descr = process_row(lines, [v.get() for v in vars_top], [v.get() for v in vars_bottom])

            self.table.update_column_widths([art], 0)
            self.table.update_column_widths([name], 1)
            self.table.update_column_widths(lines, 2)
            self.table.update_column_widths([price_str], 3)
            self.table.update_column_widths([price_wo_vat_str], 4)

            self.table.insert_row(idx_for_rowdata, art, name, descr, price_str, price_wo_vat_str,
                                  vars_top, vars_bottom, callback)

            self.rowdata.append({
                'lines': lines,
                'art': art,
                'desc_raw': desc_raw,
                'price_str': price_str,
                'price_wo_vat_str': price_wo_vat_str,
                'vars_top': vars_top,
                'vars_bottom': vars_bottom,
            })

            idx_for_rowdata += 1

        self.save_btn.config(state='normal')
        self.table.canvas.update_idletasks()
        self.table.canvas.config(scrollregion=self.table.canvas.bbox("all"))

    def update_row(self, idx, *args):
        item = self.rowdata[idx]
        lines = item['lines']
        top_state = [v.get() for v in item['vars_top']]
        bottom_state = [v.get() for v in item['vars_bottom']]
        name, descr = process_row(lines, top_state, bottom_state)

        art_lbl, name_lbl, desc_lbl, price_lbl, price_wo_vat_lbl, cbf_top, cbf_bottom = self.table.rows_widgets[idx]

        name_lbl.config(text=name)

        k_top = min(sum(top_state), len(lines))
        k_bottom = min(sum(bottom_state), max(0, len(lines) - k_top))
        descr_lines = lines[k_top:len(lines) - k_bottom if k_bottom > 0 else None]
        new_text = '\n'.join(descr_lines)
        desc_lbl.config(text=new_text)

        n_lines_desc = new_text.count('\n') + 1 if new_text else 1

        desc_lbl.config(height=n_lines_desc)
        art_lbl.config(height=n_lines_desc)
        price_lbl.config(height=n_lines_desc)
        price_wo_vat_lbl.config(height=n_lines_desc)

        pady_val = max(4, n_lines_desc * 6 // 3)
        for cb in cbf_top.winfo_children():
            cb.pack_configure(pady=pady_val)
        for cb in cbf_bottom.winfo_children():
            cb.pack_configure(pady=pady_val)

    def save(self):
        result = {
            'Артикул': [],
            'Наименование': [],
            'Описание': [],
            'РРЦ': [],
            'РРЦ (без НДС)': [],
        }
        for i, item in enumerate(self.rowdata):
            lines = item['lines']
            top_state = [v.get() for v in item['vars_top']]
            bottom_state = [v.get() for v in item['vars_bottom']]
            name, desc = process_row(lines, top_state, bottom_state)
            result['Артикул'].append(item['art'])
            result['Наименование'].append(name)
            result['Описание'].append(desc)
            result['РРЦ'].append(item['price_str'])
            result['РРЦ (без НДС)'].append(item['price_wo_vat_str'])
        dfout = pd.DataFrame(result)
        fname = filedialog.asksaveasfilename(defaultextension=".xlsx", filetypes=[("Excel files", "*.xlsx")])
        if not fname:
            return
        dfout.to_excel(fname, index=False)
        messagebox.showinfo("Готово", "Файл успешно сохранён")

if __name__ == "__main__":
    root = tk.Tk()
    app = DynamicTableApp(root)
    root.mainloop()

Сделай так, чтобы я мог загружать и выгружать данные не только со столбцом Артикул, Наименование и Описание, но и, например, с ссылкой или еще какими-нибудь данными из исходной таблицы. Также я хочу иметь возможность добавить столбцы с определенными данными для всего столбца, то есть для каждой строки в таблице.