diff --git a/README.md b/README.md index 2e71e15..598b0dd 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,28 @@ This tool is ideal for professionals who deal with MS Access databases and need 2. **[Save and Load Configurations (#17)](https://github.com/whellcome/MSAccessToSQL/issues/17)** - Add functionality to save the current export setup (e.g., database path, selected tables) and reload it in future sessions, enabling quick and consistent operations. +Here’s the "Important" section translated into English: + +--- + +## Install + + ```bash + git clone https://github.com/whellcome/MSAccessToSQL + cd MSAccessToSQL + pip install -r requirements.txt + ``` + +### Important + +This project relies on the `tkextras` module, which enhances functionality for working with the `tkinter` interface. The `tkextras` module, along with other dependencies, is included in the `requirements.txt` file. + +**Note:** If the `tkextras` module does not install automatically, you can manually install it using the following command: + + + ```bash + pip install git+https://github.com/whellcome/tkextras.git + ``` ## Usage diff --git a/code/export-msaccess-sql.py b/code/export-msaccess-sql.py index 934bd14..c7a6c4f 100644 --- a/code/export-msaccess-sql.py +++ b/code/export-msaccess-sql.py @@ -1,257 +1,8 @@ import tkinter as tk from tkinter import ttk, filedialog, messagebox import webbrowser -import pandas as pd import win32com.client - - -class WidgetsRender: - def __init__(self, render_params=None, *args, **options): - """ - Initialization of the Frame, description of the main elements - :param render_params: General parameters for the arrangement of elements can be set externally - :param args: - :param options: - """ - super().__init__(*args, **options) - if render_params is None: - render_params = dict(sticky="ew", padx=5, pady=2) - self.__render_params = render_params - - def param_prepare(self, pack_params, func="grid"): - pack_params = pack_params if pack_params else {} - united_pack_params = self.__render_params.copy() - if func == "pack": - if "sticky" in united_pack_params: - united_pack_params["anchor"] = united_pack_params.pop("sticky") - elif func == "place": - if "sticky" in united_pack_params: - united_pack_params["anchor"] = united_pack_params.pop("sticky") - united_pack_params.update(pack_params) - return united_pack_params - - def rgrid(self, obj:tk.Tk, render_params=None): - """ - Perform element creation and rendering in one command. Without creating a variable unnecessarily. - Combines general parameters for the arrangement of elements and parameters for a specific element. - :param obj: Element to rendering - :param pack_params: Dictionary with element parameters - :return: Rendered element - """ - if obj: - obj.grid(self.param_prepare(render_params,"grid")) - return obj - - def rpack(self, obj=None, render_params=None): - if obj: - obj.grid(self.param_prepare(render_params, "pack")) - return obj - - def rplace(self, obj=None, render_params=None): - if obj: - obj.grid(self.param_prepare(render_params, "place")) - return obj - - -class TreeviewDataFrame(WidgetsRender, ttk.Treeview): - - def __init__(self, parent, render_params=None, *args, **kwargs): - super().__init__(render_params, parent, *args, **kwargs) - self.df = pd.DataFrame() - self.filtered_df = pd.DataFrame() - self.bind("", self.toggle_cell) - self.svars = {"flag_symbol": { - "check": "✔", - "uncheck": " " - }} - self.svars["flag_values"] = { - self.svars["flag_symbol"]["uncheck"]: self.svars["flag_symbol"]["check"], - self.svars["flag_symbol"]["check"]: self.svars["flag_symbol"]["uncheck"] - } - - - def column(self, column, option=None, **kw): - """ - Override column method with DataFrame. - """ - result = super().column(column, option=option, **kw) - if column not in self.df.columns: - self.df[column] = '' - return result - - def insert(self, parent, index, iid=None, **kw): - """ - Inserts a new row into the Treeview and synchronizes it with the DataFrame. - :param parent: Parent node for Treeview (usually "" for root-level items). - :param index: Position to insert the item. - :param iid: Unique identifier for the row. If None, Treeview generates one. - :param kw: Additional arguments for Treeview insert (e.g., values). - """ - # Use the provided iid or let Treeview generate one - if iid is None: - iid = super().insert(parent, index, **kw) # Automatically generate iid - else: - super().insert(parent, index, iid=iid, **kw) - - # Ensure values are provided - values = kw.get("values", []) - - # Convert values to a DataFrame-compatible dictionary - new_row = {col: val for col, val in zip(self.cget("columns"), values)} - - # Add the new row to the DataFrame, using iid as the index - self.df.loc[iid] = new_row - return iid - - def set(self, item, column=None, value=None): - """ - Enhanced set method for synchronization with a DataFrame. - :param item: The item ID (iid) in the Treeview. - :param column: The column name to retrieve or update. - :param value: The value to set; if None, retrieves the current value. - :return: The value as returned by the original Treeview method. - """ - result = super().set(item, column, value) - if item not in self.df.index: - raise KeyError(f"Row with index '{item}' not found in DataFrame.") - is_filtered = True if item in self.filtered_df.index else False - if value is None: - if column is None: - self.df.loc[item] = self.df.loc[item].replace(result) - else: - self.df.loc[item, column] = result - else: - self.df.loc[item, column] = value - if is_filtered: - self.filtered_df.loc[item, column] = value - ind = self.cget("columns").index(column) if not column else 0 - self.all_checked_update(ind) - return result - - def item(self, item, option=None, **kw): - """ - Override item method with DataFrame. - """ - values = kw.get("values", []) - result = super().item(item, option, **kw) - is_filtered = True if item in self.filtered_df.index else False - if option is None and len(values): - updates = pd.Series(values, index=self.cget("columns")) - self.df.loc[item] = updates - if is_filtered: - self.filtered_df.loc[item] = updates - self.all_checked_update() - return result - - def delete(self, *items, inplace=False): - """ - Override delete method with DataFrame.. - """ - if inplace: - for item in items: - values = self.item(item, "values") - self.df = self.df[~(self.df[list(self.df.columns)] == values).all(axis=1)] - super().delete(*items) - - def flag_inverse(self, value: str) -> str: - flag_values = self.svars["flag_values"] - return flag_values[value] - - def toggle_cell(self, event): - """Handles cell clicks to change flags.""" - if self.identify_region(event.x, event.y) != "cell": - return - col_num = int(self.identify_column(event.x).replace("#", "")) - 1 - if not col_num: - return - col_name = self.cget("columns")[col_num] - item = self.identify_row(event.y) - current_value = self.set(item, col_name) - self.set(item, col_name, self.flag_inverse(current_value)) - self.event_generate("<>") - - def rebuild_tree(self, dataframe=None): - if dataframe is None: - dataframe = self.df - self.delete(*self.get_children()) - for index, row in dataframe.iterrows(): - self.insert("", "end", iid=index, values=row.to_list()) - - def filter_by_name(self, keyword=""): - """Filter rows based on a keyword and update Treeview.""" - self.filtered_df = self.df[self.df[self.df.columns[0]].str.contains(keyword, case=False)].copy() - self.rebuild_tree(self.filtered_df) - - def filter_event_evoke(self): - """Filter updated event.""" - self.event_generate("<>") - - def all_checked_event_evoke(self): - self.event_generate("<>") - - def is_all_checked(self, column): - df = self.filtered_df if len(self.filtered_df) else self.df - return not len(df[df.iloc[:, column] == self.svars["flag_symbol"]["uncheck"]]) - - def all_checked_update(self, column = 0): - if column: - self.svars['check_all'][column].set(self.is_all_checked(column)) - else: - for i in range(1,len(self.cget("columns"))): - self.svars['check_all'][i].set(self.is_all_checked(i)) - self.all_checked_event_evoke() - - def filter_widget(self, parent): - widget_frame = ttk.Frame(parent, width=150, borderwidth=1, relief="solid", padding=(2, 2)) - self.rgrid(tk.Label(widget_frame, text="Filter by table name:", font=("Helvetica", 9, "bold")), - dict(row=0, column=0, pady=5)) - filter_entry = tk.Entry(widget_frame) - self.rgrid(filter_entry, dict(row=0, column=1, padx=5, pady=5, sticky="ew")) - - def apply_filter(): - self.filter_by_name(filter_entry.get()) - if len(self.filtered_df) == len(self.df): - self.filtered_df = pd.DataFrame() - self.filter_event_evoke() - self.all_checked_update() - - def clear_filter(): - self.rebuild_tree() - filter_entry.delete(0, tk.END) - self.filtered_df = pd.DataFrame() - self.filter_event_evoke() - self.all_checked_update() - - self.rgrid(ttk.Button(widget_frame, text="Filter", command=apply_filter), - dict(row=0, column=2, padx=5, pady=5)) - self.rgrid(ttk.Button(widget_frame, text="Restore", command=clear_filter), - dict(row=0, column=3, padx=5, pady=5)) - return widget_frame - - def checkbox_widget(self, parent): - - def toggle_all(index: int): - checked = self.svars['check_all'][index].get() - if not checked: - self.svars['check_all'][index].set(False) - for item in self.get_children(): - values = list(self.item(item, "values")) - if checked: - values[index] = self.svars['flag_symbol']['check'] - else: - values[index] = self.svars['flag_symbol']['uncheck'] - self.item(item, values=values) - - widget_frame = ttk.Frame(parent, padding=(2, 2)) - self.svars["check_all"] = {} - for i, col in enumerate(self.cget("columns")[1:]): - ind = i+1 - self.svars["check_all"][ind] = tk.IntVar(value=0) - box_text = f"Check all {self.heading(col)['text'] if self.heading(col)['text'] else col}" - render_params = dict(row=0, column=ind, padx=20) - self.rgrid(ttk.Checkbutton(widget_frame, text=box_text, variable=self.svars["check_all"][ind], - command=lambda k=ind: toggle_all(k)), render_params) - return widget_frame +from tkextras import * class GetWidgetsFrame(WidgetsRender, ttk.Frame): diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..2b6f432 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +tkextras @ git+https://github.com/whellcome/tkextras.git +pandas>=1.3.0 +pywin32>=300 \ No newline at end of file