Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
251 changes: 1 addition & 250 deletions code/export-msaccess-sql.py
Original file line number Diff line number Diff line change
@@ -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("<Button-1>", 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("<<TreeToggleCell>>")

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("<<TreeFilterUpdated>>")

def all_checked_event_evoke(self):
self.event_generate("<<TreeCheckAllUpdated>>")

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):
Expand Down
3 changes: 3 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
tkextras @ git+https://github.com/whellcome/tkextras.git
pandas>=1.3.0
pywin32>=300