قبل ظهور Gradio، وقبل ظهور Streamlit، وقبل ظهور Taipy، كان هناك Tkinter. كان Tkinter ولا يزال أداة بناء واجهات المستخدم الرسومية الأصلية في بايثون، وحتى قبل بضع سنوات، كان أحد الطرق القليلة التي يمكنك من خلالها إنشاء أي نوع من لوحات المعلومات أو واجهات المستخدم الرسومية باستخدام بايثون.
مع ظهور أطر عمل جديدة قائمة على الويب مثل تلك المذكورة أعلاه والتي احتلت الصدارة في عرض تطبيقات التعلم الآلي التي تركز على البيانات على سطح المكتب، نطرح السؤال التالي: “هل لا يزال هناك مجال لاستخدام مكتبة Tkinter؟”.
إجابتي على هذا السؤال هي نعم قاطعة! آمل أن أوضح في هذه المقالة أن Tkinter لا تزال أداة قوية وخفيفة الوزن وذات صلة كبيرة لإنشاء تطبيقات واجهة المستخدم الرسومية الأصلية لسطح المكتب وتطبيقات لوحة معلومات البيانات.
بالنسبة للمطورين الذين يحتاجون إلى إنشاء أدوات داخلية، أو برامج مساعدة بسيطة، أو برامج تعليمية، يُعدّ Tkinter الخيار الأمثل. فهو لا يتطلب خوادم ويب معقدة، أو معرفة بلغة JavaScript، أو تبعيات كثيرة. إنه ببساطة لغة Python. وكما سأوضح لاحقًا، يمكنك إنشاء لوحات تحكم متطورة وعصرية المظهر باستخدامه.
في الجزء المتبقي من هذه المقالة، سننتقل من المبادئ الأساسية لـ Tkinter إلى البناء العملي للوحة تحكم ديناميكية تعتمد على البيانات، مما يثبت أن مكتبة واجهة المستخدم الرسومية “OG” هذه لا تزال تحتوي على الكثير من الحيل الحديثة.
ما هو Tkinter ولماذا يجب أن تهتم به؟
Tkinter هي مجموعة أدوات واجهة المستخدم الرسومية (GUI) القياسية والمدمجة في لغة بايثون. اسمها مشتق من عبارة “واجهة Tk”. وهي عبارة عن غلاف برمجي حول Tcl/Tk، وهي مجموعة أدوات واجهة مستخدم رسومية قوية ومتعددة المنصات موجودة منذ أوائل التسعينيات.
تتمثل ميزته الأهم في كونه جزءًا من مكتبة بايثون القياسية. هذا يعني أنه إذا كان بايثون مثبتًا لديك، فستجد Tkinter مثبتًا لديك. لا حاجة لتشغيل أوامر pip install، ولا توجد مشاكل في تبعيات البيئة الافتراضية. يعمل مباشرةً على أنظمة ويندوز، وماك أو إس، ولينكس.
إذن، لماذا نختار Tkinter في عصر أطر عمل الويب البراقة؟
- البساطة والسرعة: بالنسبة للتطبيقات الصغيرة والمتوسطة الحجم، يُعدّ Tkinter سريع التطوير. يمكنك إنشاء نافذة وظيفية بعناصر تفاعلية ببضعة أسطر من التعليمات البرمجية فقط.
- خفيفة الوزن: تتميز تطبيقات Tkinter بحجمها الصغير. فهي لا تتطلب متصفحًا أو خادم ويب، مما يجعلها مثالية للأدوات البسيطة التي تحتاج إلى التشغيل بكفاءة على أي جهاز.
- المظهر والشعور الأصلي (إلى حد ما): في حين أن Tkinter الكلاسيكي له مظهر قديم معروف، فإن مجموعة الأدوات ذات الطابع ttk توفر الوصول إلى عناصر تحكم أكثر حداثة وذات مظهر أصلي تتناسب بشكل أفضل مع نظام التشغيل المضيف.
- ممتاز للتعلم: يُعلّم Tkinter المفاهيم الأساسية للبرمجة القائمة على الأحداث، وهي جوهر تطوير واجهات المستخدم الرسومية. إن فهم كيفية إدارة الأدوات والتخطيطات وأحداث المستخدم في Tkinter يُوفر أساسًا متينًا لتعلم أي إطار عمل آخر لواجهات المستخدم الرسومية.
بالطبع، له عيوبه. فالتطبيقات المعقدة ذات التصميم الجذاب قد يكون بناؤها صعباً، وقد تبدو فلسفة تصميمها أكثر إسهاباً مقارنةً بأسلوب Streamlit أو Gradio التصريحي. مع ذلك، فهو يتفوق في غرضه الأساسي، وهو إنشاء تطبيقات سطح مكتب وظيفية ومستقلة.
مع مرور الوقت، ظهرت مكتبات إضافية تُضفي على واجهات المستخدم الرسومية المبنية على Tkinter مظهرًا أكثر حداثة. إحدى هذه المكتبات، والتي سنستخدمها، تُسمى ttkbootstrap. هذه المكتبة مبنية على Tkinter، وتضيف عناصر واجهة مستخدم إضافية، وتمنح واجهات المستخدم الرسومية مظهرًا مستوحى من Bootstrap.
المفاهيم الأساسية لتطبيق Tkinter
تُبنى جميع تطبيقات Tkinter على بضعة ركائز أساسية. فهم هذه المفاهيم ضروري قبل أن تتمكن من إنشاء أي شيء ذي قيمة.
1/ نافذة الجذر
النافذة الرئيسية هي الحاوية الأساسية لتطبيقك بالكامل. وهي النافذة العليا التي تحتوي على شريط عنوان، وأزرار تصغير وتكبير وإغلاق. يمكنك إنشاؤها بسطر واحد من التعليمات البرمجية كما يلي.
import tkinter as tk
root = tk.Tk()
root.title("My First Tkinter App")
root.mainloop()
ينتج الكود هذا. ليس الأمر مثيراً للاهتمام، ولكنه بداية.

كل شيء آخر في تطبيقك – الأزرار، والملصقات، وحقول الإدخال، وما إلى ذلك – سيتواجد داخل هذه النافذة الجذرية.
2/ الأدوات
تُعدّ الأدوات (Widgets) اللبنات الأساسية لواجهة المستخدم الرسومية (GUI). وهي العناصر التي يراها المستخدم ويتفاعل معها. ومن أكثر الأدوات شيوعًا ما يلي:
- التسمية: تعرض نصًا ثابتًا أو صورًا.
- الزر: زر قابل للنقر يمكنه تشغيل وظيفة معينة.
- حقل الإدخال: حقل إدخال نصي من سطر واحد.
- نص: منطقة إدخال وعرض نص متعدد الأسطر.
- الإطار: حاوية مستطيلة غير مرئية تُستخدم لتجميع عناصر واجهة المستخدم الأخرى. وهذا أمر بالغ الأهمية لتنظيم التصميمات المعقدة.
- لوحة الرسم: أداة متعددة الاستخدامات لرسم الأشكال، وإنشاء الرسوم البيانية، أو عرض الصور.
3/ مديرو الهندسة
بعد إنشاء عناصر واجهة المستخدم، عليك تحديد مكان وضعها داخل النافذة باستخدام Tkinter. هذه هي مهمة مديري الهندسة. لاحظ أنه لا يمكنك استخدام مديري هندسة مختلفين ضمن نفس الحاوية الرئيسية (مثل العنصر الجذر أو الإطار).
- دالة ()pack: أبسط أنواع إدارة العناصر. تقوم بتجميع العناصر في النافذة، إما عموديًا أو أفقيًا. وهي سريعة في تصميمات بسيطة ولكنها لا توفر تحكمًا دقيقًا.
- دالة ()place: هي أدقّ أداة لإدارة العناصر. فهي تتيح لك تحديد إحداثيات البكسل (x, y) وحجم (العرض، الارتفاع) العنصر بدقة. يُنصح عمومًا بتجنب استخدامها لأنها تجعل تطبيقك جامدًا وغير متجاوب مع تغيير حجم النافذة.
- ()grid: أداة إدارة قوية ومرنة للغاية، وهي التي سنستخدمها في لوحة التحكم الخاصة بنا. فهي تنظم الأدوات في بنية تشبه الجدول تتكون من صفوف وأعمدة، مما يجعلها مثالية لإنشاء تصميمات متناسقة ومنظمة.
4/ المسار الرئيسي
يُعدّ السطر root.mainloop() الجزء الأخير والأكثر أهمية في أي تطبيق Tkinter. تبدأ هذه الدالة حلقة الأحداث، حيث يدخل التطبيق في حالة انتظار، مُستمعًا إلى تفاعلات المستخدم مثل نقرات الماوس، وضغطات المفاتيح، أو تغيير حجم النافذة. عند وقوع حدث ما، يقوم Tkinter بمعالجته (على سبيل المثال، استدعاء دالة مرتبطة بنقرة زر) ثم يعود إلى الحلقة. لن يُغلق التطبيق إلا عند انتهاء هذه الحلقة، وعادةً ما يكون ذلك بإغلاق النافذة.
إعداد بيئة التطوير
قبل أن نبدأ بالبرمجة، دعونا نُهيئ بيئة التطوير. أنا بصدد التحول تدريجياً إلى أداة سطر الأوامر UV لإعداد بيئتي، بدلاً من conda، وهذا ما سنستخدمه هنا.
# initialise our project
uv init tktest
cd tktest
# create a new venv
uv venv tktest
# switch to it
tktest\Scripts\activate
# Install required external libraries
(tktest) uv pip install matplotlib ttkbootstrap pandas
مثال 1: تطبيق بسيط “مرحباً، Tkinter!”
لنطبق هذه المفاهيم عملياً. سننشئ نافذة تحتوي على تسمية وزر. عند النقر على الزر، سيتغير نص التسمية.
import tkinter as tk
# 1. Create the root window
root = tk.Tk()
root.title("Simple Interactive App")
root.geometry("300x150") # Set window size: width x height
# This function will be called when the button is clicked
def on_button_click():
# Update the text of the label widget
label.config(text="Hello, Tkinter!")
# 2. Create the widgets
label = tk.Label(root, text="Click the button below.")
button = tk.Button(root, text="Click Me!", command=on_button_click)
# 3. Use a geometry manager to place the widgets
# We use pack() for this simple layout
label.pack(pady=20) # pady adds some vertical padding
button.pack()
# 4. Start the main event loop
root.mainloop()
ينبغي أن يبدو الأمر هكذا، مع الصورة الموجودة على اليمين والتي تمثل ما ستحصل عليه عند النقر على الزر.

حتى الآن، يبدو الأمر بسيطًا؛ ومع ذلك، يمكنك إنشاء واجهات مستخدم رسومية ولوحات تحكم حديثة وجذابة بصريًا باستخدام Tkinter. ولتوضيح ذلك، سنقوم بإنشاء تطبيق أكثر شمولًا وتعقيدًا يُظهر إمكانيات Tkinter.
مثال 2 – لوحة بيانات حديثة
في هذا المثال، سنقوم بإنشاء لوحة بيانات باستخدام مجموعة بيانات من موقع Kaggle تُسمى CarsForSale. تأتي هذه المجموعة بترخيص CC0:Public Domain، مما يعني أنه يمكن استخدامها بحرية لمعظم الأغراض.
هي مجموعة بيانات تركز على الولايات المتحدة، وتحتوي على تفاصيل المبيعات والأداء لما يقرب من 9300 طراز سيارة مختلف من حوالي 40 شركة مصنعة مختلفة، تغطي الفترة من 2001 إلى 2022. يمكنك الحصول عليها باستخدام الرابط أدناه:
https://www.kaggle.com/datasets/chancev/carsforsale/data
قم بتنزيل مجموعة البيانات واحفظها في ملف CSV على جهازك المحلي.
ملاحظة: يتم توفير مجموعة البيانات هذه بموجب ترخيص CC0: الملكية العامة، لذلك لا بأس باستخدامها في هذا السياق.

سيكون هذا المثال أكثر تعقيدًا من المثال الأول، لكنني أردت أن أعطيكم فكرة واضحة عما يمكن تحقيقه باستخدام Tkinter، لذا إليكم التفاصيل. سأعرض الكود وأشرح وظائفه العامة قبل أن نستعرض واجهة المستخدم الرسومية التي ينتجها.
###############################################################################
# USED-CAR MARKETPLACE DASHBOARD
#
#
###############################################################################
import tkinter as tk
import ttkbootstrap as tb
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import matplotlib.pyplot as plt
from matplotlib.ticker import MaxNLocator
import pandas as pd, numpy as np, re, sys
from pathlib import Path
from textwrap import shorten
# ───────────────────────── CONFIG ──────────────────────────
CSV_PATH = r"C:\Users\thoma\temp\carsforsale.csv"
COLUMN_ALIASES = {
"brand": "make", "manufacturer": "make", "carname": "model",
"rating": "consumerrating", "safety": "reliabilityrating",
}
REQUIRED = {"make", "price"}
# ──────────────────────────────────────────────────────────────
class Dashboard:
# ═══════════════════════════════════════════════════════════
def __init__(self, root: tb.Window):
self.root = root
self.style = tb.Style("darkly")
self._make_spinbox_style()
self.clr = self.style.colors
self.current_analysis_plot_func = None
self._load_data()
self._build_gui()
self._apply_filters()
# ─────────── spin-box style (white arrows) ────────────────
def _make_spinbox_style(self):
try:
self.style.configure("White.TSpinbox",
arrowcolor="white",
arrowsize=12)
self.style.map("White.TSpinbox",
arrowcolor=[("disabled", "white"),
("active", "white"),
("pressed", "white")])
except tk.TclError:
pass
# ───────────────────── DATA LOAD ───────────────────────────
def _load_data(self):
csv = Path(CSV_PATH)
if not csv.exists():
tb.dialogs.Messagebox.show_error("CSV not found", str(csv))
sys.exit()
df = pd.read_csv(csv, encoding="utf-8-sig", skipinitialspace=True)
df.columns = [
COLUMN_ALIASES.get(
re.sub(r"[^0-9a-z]", "", c.lower().replace("\ufeff", "")),
c.lower()
)
for c in df.columns
]
if "year" not in df.columns:
for col in df.columns:
nums = pd.to_numeric(df[col], errors="coerce")
if nums.dropna().between(1900, 2035).all():
df.rename(columns={col: "year"}, inplace=True)
break
for col in ("price", "minmpg", "maxmpg",
"year", "mileage", "consumerrating"):
if col in df.columns:
df[col] = pd.to_numeric(
df[col].astype(str)
.str.replace(r"[^\d.]", "", regex=True),
errors="coerce"
)
if any(c not in df.columns for c in REQUIRED):
tb.dialogs.Messagebox.show_error(
"Bad CSV", "Missing required columns.")
sys.exit()
self.df = df.dropna(subset=["make", "price"])
# ───────────────────── GUI BUILD ───────────────────────────
def _build_gui(self):
header = tb.Frame(self.root, width=600, height=60, bootstyle="dark")
header.pack_propagate(False)
header.pack(side="top", anchor="w", padx=8, pady=(4, 2))
tb.Label(header, text="🚗 USED-CAR DASHBOARD",
font=("Segoe UI", 16, "bold"), anchor="w")\
.pack(fill="both", padx=8, pady=4)
self.nb = tb.Notebook(self.root); self.nb.pack(fill="both", expand=True)
self._overview_tab()
self._analysis_tab()
self._data_tab()
# ───────────────── OVERVIEW TAB ─────────────────────────
def _overview_tab(self):
tab = tb.Frame(self.nb); self.nb.add(tab, text="Overview")
self._filters(tab)
self._cards(tab)
self._overview_fig(tab)
def _spin(self, parent, **kw):
return tb.Spinbox(parent, style="White.TSpinbox", **kw)
def _filters(self, parent):
f = tb.Labelframe(parent, text="Filters", padding=6)
f.pack(fill="x", padx=8, pady=6)
tk.Label(f, text="Make").grid(row=0, column=0, sticky="w", padx=4)
self.make = tk.StringVar(value="All")
tb.Combobox(f, textvariable=self.make, state="readonly", width=14,
values=["All"] + sorted(self.df["make"].unique()),
bootstyle="dark")\
.grid(row=0, column=1)
self.make.trace_add("write", self._apply_filters)
if "drivetrain" in self.df.columns:
tk.Label(f, text="Drivetrain").grid(row=0, column=2, padx=(20, 4))
self.drive = tk.StringVar(value="All")
tb.Combobox(f, textvariable=self.drive, state="readonly", width=14,
values=["All"] + sorted(self.df["drivetrain"].dropna()
.unique()),
bootstyle="dark")\
.grid(row=0, column=3)
self.drive.trace_add("write", self._apply_filters)
pr_min, pr_max = self.df["price"].min(), self.df["price"].max()
tk.Label(f, text="Price $").grid(row=0, column=4, padx=(20, 4))
self.pmin = tk.DoubleVar(value=float(pr_min))
self.pmax = tk.DoubleVar(value=float(pr_max))
for col, var in [(5, self.pmin), (6, self.pmax)]:
self._spin(f, from_=0, to=float(pr_max), textvariable=var,
width=10, increment=1000, bootstyle="secondary")\
.grid(row=0, column=col)
if "year" in self.df.columns:
yr_min, yr_max = int(self.df["year"].min()), int(self.df["year"].max())
tk.Label(f, text="Year").grid(row=0, column=7, padx=(20, 4))
self.ymin = tk.IntVar(value=yr_min)
self.ymax = tk.IntVar(value=yr_max)
for col, var in [(8, self.ymin), (9, self.ymax)]:
self._spin(f, from_=1900, to=2035, textvariable=var,
width=6, bootstyle="secondary")\
.grid(row=0, column=col)
tb.Button(f, text="Apply Year/Price Filters",
bootstyle="primary-outline",
command=self._apply_filters)\
.grid(row=0, column=10, padx=(30, 4))
def _cards(self, parent):
wrap = tb.Frame(parent); wrap.pack(fill="x", padx=8)
self.cards = {}
for lbl in ("Total Cars", "Average Price",
"Average Mileage", "Avg Rating"):
card = tb.Frame(wrap, padding=6, relief="ridge", bootstyle="dark")
card.pack(side="left", fill="x", expand=True, padx=4, pady=4)
val = tb.Label(card, text="-", font=("Segoe UI", 16, "bold"),
foreground=self.clr.info)
val.pack()
tb.Label(card, text=lbl, foreground="white").pack()
self.cards[lbl] = val
def _overview_fig(self, parent):
fr = tb.Frame(parent); fr.pack(fill="both", expand=True, padx=8, pady=6)
self.ov_fig = plt.Figure(figsize=(18, 10), facecolor="#1e1e1e",
constrained_layout=True)
self.ov_canvas = FigureCanvasTkAgg(self.ov_fig, master=fr)
self.ov_canvas.get_tk_widget().pack(fill="both", expand=True)
# ───────────────── ANALYSIS TAB ──────────────────────────
def _analysis_tab(self):
tab = tb.Frame(self.nb); self.nb.add(tab, text="Analysis")
ctl = tb.Frame(tab); ctl.pack(fill="x", padx=8, pady=6)
def set_and_run_analysis(plot_function):
self.current_analysis_plot_func = plot_function
plot_function()
for txt, fn in (("Correlation", self._corr),
("Price by Make", self._price_make),
("MPG", self._mpg),
("Ratings", self._ratings)):
tb.Button(ctl, text=txt, command=lambda f=fn: set_and_run_analysis(f),
bootstyle="info-outline").pack(side="left", padx=4)
self.an_fig = plt.Figure(figsize=(12, 7), facecolor="#1e1e1e",
constrained_layout=True)
self.an_canvas = FigureCanvasTkAgg(self.an_fig, master=tab)
w = self.an_canvas.get_tk_widget()
w.configure(width=1200, height=700)
w.pack(padx=8, pady=4)
# ───────────────── DATA TAB ────────────────────────────────
def _data_tab(self):
tab = tb.Frame(self.nb); self.nb.add(tab, text="Data")
top = tb.Frame(tab); top.pack(fill="x", padx=8, pady=6)
tk.Label(top, text="Search").pack(side="left")
self.search = tk.StringVar()
tk.Entry(top, textvariable=self.search, width=25)\
.pack(side="left", padx=4)
self.search.trace_add("write", self._search_tree)
cols = list(self.df.columns)
self.tree = tb.Treeview(tab, columns=cols, show="headings",
bootstyle="dark")
for c in cols:
self.tree.heading(c, text=c.title())
self.tree.column(c, width=120, anchor="w")
ysb = tb.Scrollbar(tab, orient="vertical", command=self.tree.yview)
xsb = tb.Scrollbar(tab, orient="horizontal", command=self.tree.xview)
self.tree.configure(yscroll=ysb.set, xscroll=xsb.set)
self.tree.pack(side="left", fill="both", expand=True)
ysb.pack(side="right", fill="y"); xsb.pack(side="bottom", fill="x")
# ───────────────── FILTER & STATS ──────────────────────────
def _apply_filters(self, *_):
df = self.df.copy()
if self.make.get() != "All":
df = df[df["make"] == self.make.get()]
if hasattr(self, "drive") and self.drive.get() != "All":
df = df[df["drivetrain"] == self.drive.get()]
try:
pmin, pmax = float(self.pmin.get()), float(self.pmax.get())
except ValueError:
pmin, pmax = df["price"].min(), df["price"].max()
df = df[(df["price"] >= pmin) & (df["price"] <= pmax)]
if "year" in df.columns and hasattr(self, "ymin"):
try:
ymin, ymax = int(self.ymin.get()), int(self.ymax.get())
except ValueError:
ymin, ymax = df["year"].min(), df["year"].max()
df = df[(df["year"] >= ymin) & (df["year"] <= ymax)]
self.filtered = df
self._update_cards()
self._draw_overview()
self._fill_tree()
if self.current_analysis_plot_func:
self.current_analysis_plot_func()
def _update_cards(self):
d = self.filtered
self.cards["Total Cars"].configure(text=f"{len(d):,}")
self.cards["Average Price"].configure(
text=f"${d['price'].mean():,.0f}" if not d.empty else "$0")
m = d["mileage"].mean() if "mileage" in d.columns else np.nan
self.cards["Average Mileage"].configure(
text=f"{m:,.0f} mi" if not np.isnan(m) else "-")
r = d["consumerrating"].mean() if "consumerrating" in d.columns else np.nan
self.cards["Avg Rating"].configure(
text=f"{r:.2f}" if not np.isnan(r) else "-")
# ───────────────── OVERVIEW PLOTS (clickable) ──────────────
def _draw_overview(self):
if hasattr(self, "_ov_pick_id"):
self.ov_fig.canvas.mpl_disconnect(self._ov_pick_id)
self.ov_fig.clear()
self._ov_annot = None
df = self.filtered
if df.empty:
ax = self.ov_fig.add_subplot(111)
ax.axis("off")
ax.text(0.5, 0.5, "No data", ha="center", va="center", color="white", fontsize=16)
self.ov_canvas.draw(); return
gs = self.ov_fig.add_gridspec(2, 2)
ax_hist = self.ov_fig.add_subplot(gs[0, 0])
ax_scatter = self.ov_fig.add_subplot(gs[0, 1])
ax_pie = self.ov_fig.add_subplot(gs[1, 0])
ax_bar = self.ov_fig.add_subplot(gs[1, 1])
ax_hist.hist(df["price"], bins=30, color=self.clr.info)
ax_hist.set_title("Price Distribution", color="w")
ax_hist.set_xlabel("Price ($)", color="w"); ax_hist.set_ylabel("Cars", color="w")
ax_hist.tick_params(colors="w")
df_scatter_data = df.dropna(subset=["mileage", "price"])
self._ov_scatter_map = {}
if not df_scatter_data.empty:
sc = ax_scatter.scatter(df_scatter_data["mileage"], df_scatter_data["price"],
s=45, alpha=0.8, c=df_scatter_data["year"], cmap="viridis")
sc.set_picker(True); sc.set_pickradius(10)
self._ov_scatter_map[sc] = df_scatter_data.reset_index(drop=True)
cb = self.ov_fig.colorbar(sc, ax=ax_scatter)
cb.ax.yaxis.set_major_locator(MaxNLocator(integer=True))
cb.ax.tick_params(colors="w"); cb.set_label("Year", color="w")
def _on_pick(event):
if len(event.ind) == 0:
return
row = self._ov_scatter_map[event.artist].iloc[event.ind[0]]
label = shorten(f"{row['make']} {row.get('model','')}", width=40, placeholder="…")
if self._ov_annot:
self._ov_annot.remove()
self._ov_annot = ax_scatter.annotate(
label, (row["mileage"], row["price"]),
xytext=(10, 10), textcoords="offset points",
bbox=dict(boxstyle="round", fc="white", alpha=0.9), color="black")
self.ov_canvas.draw_idle()
self._ov_pick_id = self.ov_fig.canvas.mpl_connect("pick_event", _on_pick)
ax_scatter.set_title("Mileage vs Price", color="w")
ax_scatter.set_xlabel("Mileage", color="w"); ax_scatter.set_ylabel("Price ($)", color="w")
ax_scatter.tick_params(colors="w")
if "drivetrain" in df.columns:
cnt = df["drivetrain"].value_counts()
if not cnt.empty:
ax_pie.pie(cnt, labels=cnt.index, autopct="%1.0f%%", textprops={'color': 'w'})
ax_pie.set_title("Cars by Drivetrain", color="w")
if not df.empty:
top = df.groupby("make")["price"].mean().nlargest(10).sort_values()
if not top.empty:
top.plot(kind="barh", ax=ax_bar, color=self.clr.primary)
ax_bar.set_title("Top-10 Makes by Avg Price", color="w")
ax_bar.set_xlabel("Average Price ($)", color="w"); ax_bar.set_ylabel("Make", color="w")
ax_bar.tick_params(colors="w")
self.ov_canvas.draw()
# ───────────────── ANALYSIS PLOTS ──────────────────────────
def _corr(self):
self.an_fig.clear()
ax = self.an_fig.add_subplot(111)
num = self.filtered.select_dtypes(include=np.number)
if num.shape[1] < 2:
ax.text(0.5, 0.5, "Not Enough Numeric Data", ha="center", va="center", color="white", fontsize=16)
ax.axis('off')
self.an_canvas.draw(); return
im = ax.imshow(num.corr(), cmap="RdYlBu_r", vmin=-1, vmax=1)
ax.set_xticks(range(num.shape[1])); ax.set_yticks(range(num.shape[1]))
ax.set_xticklabels(num.columns, rotation=45, ha="right", color="w")
ax.set_yticklabels(num.columns, color="w")
cb = self.an_fig.colorbar(im, ax=ax, fraction=0.046)
cb.ax.tick_params(colors="w"); cb.set_label("Correlation", color="w")
ax.set_title("Feature Correlation Heat-map", color="w")
self.an_canvas.draw()
def _price_make(self):
self.an_fig.clear()
ax = self.an_fig.add_subplot(111)
df = self.filtered
if df.empty or {"make","price"}.issubset(df.columns) is False:
ax.text(0.5, 0.5, "No Data for this Filter", ha="center", va="center", color="white", fontsize=16)
ax.axis('off')
self.an_canvas.draw(); return
makes = df["make"].value_counts().nlargest(15).index
if makes.empty:
ax.text(0.5, 0.5, "No Makes to Display", ha="center", va="center", color="white", fontsize=16)
ax.axis('off')
self.an_canvas.draw(); return
data = [df[df["make"] == m]["price"] for m in makes]
# ### FIX: Use 'labels' instead of 'tick_labels' ###
ax.boxplot(data, labels=makes, vert=False, patch_artist=True,
boxprops=dict(facecolor=self.clr.info),
medianprops=dict(color=self.clr.danger))
ax.set_title("Price Distribution by Make", color="w")
ax.set_xlabel("Price ($)", color="w"); ax.set_ylabel("Make", color="w")
ax.tick_params(colors="w")
self.an_canvas.draw()
def _ratings(self):
self.an_fig.clear()
ax = self.an_fig.add_subplot(111)
cols = [c for c in (
"consumerrating","comfortrating","interiordesignrating",
"performancerating","valueformoneyrating","reliabilityrating")
if c in self.filtered.columns]
if not cols:
ax.text(0.5, 0.5, "No Rating Data in CSV", ha="center", va="center", color="white", fontsize=16)
ax.axis('off')
self.an_canvas.draw(); return
data = self.filtered[cols].dropna()
if data.empty:
ax.text(0.5, 0.5, "No Rating Data for this Filter", ha="center", va="center", color="white", fontsize=16)
ax.axis('off')
self.an_canvas.draw(); return
ax.boxplot(data.values,
labels=[c.replace("rating","") for c in cols],
patch_artist=True,
boxprops=dict(facecolor=self.clr.warning),
medianprops=dict(color=self.clr.danger))
ax.set_title("Ratings Distribution", color="w")
ax.set_ylabel("Rating (out of 5)", color="w"); ax.set_xlabel("Rating Type", color="w")
ax.tick_params(colors="w", rotation=45)
self.an_canvas.draw()
def _mpg(self):
if hasattr(self, "_mpg_pick_id"):
self.an_fig.canvas.mpl_disconnect(self._mpg_pick_id)
self.an_fig.clear()
ax = self.an_fig.add_subplot(111)
self._mpg_annot = None
raw = self.filtered
if {"minmpg","maxmpg","make"}.issubset(raw.columns) is False:
ax.text(0.5,0.5,"No MPG Data in CSV",ha="center",va="center",color="w", fontsize=16)
ax.axis('off')
self.an_canvas.draw(); return
df = raw.dropna(subset=["minmpg","maxmpg"])
if df.empty:
ax.text(0.5,0.5,"No MPG Data for this Filter",ha="center",va="center",color="w", fontsize=16)
ax.axis('off')
self.an_canvas.draw(); return
top = df["make"].value_counts().nlargest(6).index
palette = plt.cm.tab10.colors
self._scatter_map = {}
rest = df[~df["make"].isin(top)]
if not rest.empty:
sc = ax.scatter(rest["minmpg"], rest["maxmpg"],
s=25, c="lightgrey", alpha=.45, label="Other")
sc.set_picker(True); sc.set_pickradius(10)
self._scatter_map[sc] = rest.reset_index(drop=True)
for i, mk in enumerate(top):
sub = df[df["make"] == mk]
sc = ax.scatter(sub["minmpg"], sub["maxmpg"],
s=35, color=palette[i % 10], label=mk, alpha=.8)
sc.set_picker(True); sc.set_pickradius(10)
self._scatter_map[sc] = sub.reset_index(drop=True)
def _on_pick(event):
if len(event.ind) == 0:
return
row = self._scatter_map[event.artist].iloc[event.ind[0]]
label = shorten(f"{row['make']} {row.get('model','')}", width=40, placeholder="…")
if self._mpg_annot: self._mpg_annot.remove()
self._mpg_annot = ax.annotate(
label, (row["minmpg"], row["maxmpg"]),
xytext=(10, 10), textcoords="offset points",
bbox=dict(boxstyle="round", fc="white", alpha=0.9), color="black")
self.an_canvas.draw_idle()
self._mpg_pick_id = self.an_fig.canvas.mpl_connect("pick_event", _on_pick)
try:
best_hwy = df.loc[df["maxmpg"].idxmax()]
best_city = df.loc[df["minmpg"].idxmax()]
for r, t in [(best_hwy, "Best Hwy"), (best_city, "Best City")]:
ax.annotate(
f"{t}: {shorten(r['make']+' '+str(r.get('model','')),28, placeholder='…')}",
xy=(r["minmpg"], r["maxmpg"]),
xytext=(5, 5), textcoords="offset points",
fontsize=7, color="w", backgroundcolor="#00000080")
except (ValueError, KeyError): pass
ax.set_title("City MPG vs Highway MPG", color="w")
ax.set_xlabel("City MPG", color="w"); ax.set_ylabel("Highway MPG", color="w")
ax.tick_params(colors="w")
if len(top) > 0:
ax.legend(facecolor="#1e1e1e", framealpha=.3, fontsize=8, labelcolor="w", loc="upper left")
self.an_canvas.draw()
# ───────────── TABLE / SEARCH / EXPORT ─────────────────────
def _fill_tree(self):
self.tree.delete(*self.tree.get_children())
for _, row in self.filtered.head(500).iterrows():
vals = [f"{v:,.2f}" if isinstance(v, float)
else f"{int(v):,}" if isinstance(v, (int, np.integer)) else v
for v in row]
self.tree.insert("", "end", values=vals)
def _search_tree(self, *_):
term = self.search.get().lower()
self.tree.delete(*self.tree.get_children())
if not term: self._fill_tree(); return
mask = self.filtered.astype(str).apply(
lambda s: s.str.lower().str.contains(term, na=False)).any(axis=1)
for _, row in self.filtered[mask].head(500).iterrows():
vals = [f"{v:,.2f}" if isinstance(v, float)
else f"{int(v):,}" if isinstance(v, (int, np.integer)) else v
for v in row]
self.tree.insert("", "end", values=vals)
def _export(self):
fn = tb.dialogs.filedialog.asksaveasfilename(
defaultextension=".csv", filetypes=[("CSV", "*.csv")])
if fn:
self.filtered.to_csv(fn, index=False)
tb.dialogs.Messagebox.show_info("Export complete", fn)
# ═══════════════════════════════════════════════════════════════
if __name__ == "__main__":
root = tb.Window(themename="darkly")
Dashboard(root)
root.mainloop()
وصف عالي المستوى للتعليمات البرمجية ومجموعة التقنيات
يُنشئ هذا البرنامج النصي المكتوب بلغة بايثون لوحة تحكم رسومية شاملة وتفاعلية للغاية، مصممة لتحليل بيانات السيارات المستعملة. تم بناؤه كتطبيق سطح مكتب مستقل باستخدام مجموعة من المكتبات القوية. توفر مكتبة Tkinter، عبر غلاف ttkbootstrap، مكونات واجهة المستخدم الرسومية الحديثة ذات الطابع المميز، بالإضافة إلى إدارة النوافذ. تتولى مكتبة pandas معالجة البيانات وتجميعها بكفاءة في الخلفية. يتم إنشاء جميع تصورات البيانات بواسطة مكتبة matplotlib، وتُدمج بسلاسة في نافذة Tkinter باستخدام واجهة FigureCanvasTkAgg الخلفية، مما يسمح بإنشاء رسوم بيانية تفاعلية معقدة ضمن إطار التطبيق. تم تصميم التطبيق ضمن فئة Dashboard واحدة، تُغلف جميع حالاته وأساليبه، مما يوفر بنية منظمة وواضحة.
استيعاب البيانات ومعالجتها المسبقة
عند بدء التشغيل، يقوم التطبيق بتنفيذ عملية تحميل وتنظيف بيانات قوية. يقرأ ملف CSV محدد باستخدام مكتبة pandas، وينفذ على الفور عدة خطوات معالجة مسبقة لضمان جودة البيانات واتساقها.
- توحيد أسماء الأعمدة: يقوم هذا الإجراء بتكرار جميع أسماء الأعمدة، وتحويلها إلى أحرف صغيرة، وإزالة الأحرف الخاصة. وهذا يمنع الأخطاء الناتجة عن عدم اتساق التسمية، مثل “Price” مقابل “price”.
- تسمية الأعمدة البديلة: تستخدم هذه الخاصية قاموسًا مُعدًا مسبقًا لإعادة تسمية أسماء الأعمدة البديلة الشائعة (مثل “العلامة التجارية” أو “الشركة المصنعة”) إلى اسم داخلي قياسي (مثل “الشركة المصنعة”). يُضفي هذا مزيدًا من المرونة، مما يسمح للتطبيق بالعمل مع تنسيقات CSV المختلفة دون الحاجة إلى تعديلات برمجية.
- الكشف الذكي عن “السنة“: إذا لم يتم العثور على عمود “السنة” بشكل صريح، يقوم البرنامج النصي بفحص الأعمدة الأخرى بذكاء للعثور على عمود يحتوي على أرقام تقع ضمن نطاق سنة نموذجي للسيارات (1900-2035)، ويحدده تلقائيًا على أنه عمود “السنة”.
- تحويل النوع: يقوم هذا الإجراء بتنظيف الأعمدة المتوقع أن تكون رقمية (مثل السعر والمسافة المقطوعة) بشكل منهجي عن طريق إزالة الأحرف غير الرقمية (مثل “$” و “,” و “mi”) وتحويل النتائج إلى أرقام عشرية، مع التعامل بسلاسة مع أي أخطاء في التحويل.
- تقليم البيانات: وأخيرًا، يقوم بإزالة أي صفوف تفتقر إلى نقاط البيانات الأساسية (الشركة المصنعة والسعر)، مما يضمن أن جميع البيانات المستخدمة في الرسم والتحليل صالحة.
واجهة المستخدم والتصفية التفاعلية
تم تنظيم واجهة المستخدم في دفتر ملاحظات رئيسي بثلاث علامات تبويب مميزة، مما يوفر سير عمل مباشر للتحليل.
- من أبرز ميزات التطبيق لوحة التصفية الديناميكية. تحتوي هذه اللوحة على عناصر واجهة مستخدم مثل قائمة منسدلة لأنواع السيارات، وعناصر تحكم دوارة لتحديد نطاقات الأسعار والسنوات. وترتبط هذه العناصر مباشرةً بالمنطق الأساسي للتطبيق.
- إدارة الحالة: عند تغيير المستخدم لأحد عوامل التصفية، يتم استدعاء الدالة المركزية apply_filters_. تقوم هذه الدالة بإنشاء إطار بيانات مؤقت جديد من نوع Pandas باسم self.filtered، وذلك بتطبيق اختيارات المستخدم على مجموعة البيانات الرئيسية. يصبح إطار البيانات self.filtered المصدر الوحيد الموثوق لجميع المكونات المرئية.
- تحديث واجهة المستخدم تلقائيًا: بعد تصفية البيانات، تُجري دالة
_apply_filtersتحديثًا كاملًا للوحة المعلومات من خلال استدعاء جميع دوال التحديث اللازمة. يشمل ذلك إعادة رسم جميع المخططات في علامة التبويب “نظرة عامة”، وتحديث بطاقات مؤشرات الأداء الرئيسية، وإعادة ملء جدول البيانات، والأهم من ذلك، إعادة رسم المخطط النشط حاليًا في علامة التبويب “تحليل”. يُوفر هذا تجربة مستخدم سلسة وسريعة الاستجابة.
علامات تبويب التصور والتحليل
تكمن القيمة الأساسية للتطبيق في قدراته على عرض البيانات، والتي تتوزع على علامتي تبويب:
1/ علامة التبويب “نظرة عامة”: تُعد هذه العلامة بمثابة لوحة المعلومات الرئيسية، وتتضمن ما يلي:
- بطاقات مؤشرات الأداء الرئيسية: تعرض أربع بطاقات بارزة في الأعلى مقاييس رئيسية مثل “إجمالي السيارات” و”متوسط السعر”، والتي يتم تحديثها في الوقت الفعلي مع الفلاتر.
- شبكة رسوم بيانية 2×2: يعرض شكل كبير متعدد الأجزاء أربعة رسوم بيانية في آن واحد: رسم بياني لتوزيع الأسعار، ورسم بياني دائري لأنواع أنظمة الدفع، ورسم بياني شريطي أفقي لأفضل 10 علامات تجارية حسب متوسط السعر، ورسم بياني مبعثر تفاعلي يوضح العلاقة بين عدد الكيلومترات المقطوعة للسيارة وسعرها، مُلوّنًا حسب السنة. يؤدي النقر على أي نقطة في هذا الرسم البياني المبعثر إلى ظهور شرح يوضح ماركة السيارة وطرازها. يتم تحقيق هذه التفاعلية من خلال ربط حدث pick_event في مكتبة Matplotlib بدالة معالجة ترسم الشرح.
٢/ علامة تبويب التحليل: تُستخدم هذه العلامة لتحليل أكثر تركيزًا على رسم بياني واحد. يتيح صف من الأزرار للمستخدم اختيار أحد خيارات العرض المرئي المتقدمة المتعددة:
- خريطة الارتباط الحراري: توضح الارتباط بين جميع الأعمدة الرقمية في مجموعة البيانات.
- مخطط الصندوق حسب العلامة التجارية: يقارن توزيعات الأسعار لأكثر 15 علامة تجارية شيوعًا للسيارات، مما يوفر نظرة ثاقبة على تباين الأسعار والقيم الشاذة.
- مخطط الصندوق للتقييمات: يعرض ويقارن توزيعات فئات تقييم المستهلك المختلفة (مثل الراحة والأداء والموثوقية).
- مخطط التشتت MPG: مخطط تشتت تفاعلي بالكامل لتحليل استهلاك الوقود في المدينة مقابل استهلاك الوقود على الطرق السريعة، مع تلوين النقاط حسب الماركة وميزة النقر لإضافة التعليقات التوضيحية المشابهة لتلك الموجودة في علامة التبويب “نظرة عامة”.
3/ علامة تبويب البيانات: بالنسبة للمستخدمين الذين يرغبون في معاينة الأرقام الأولية، تعرض هذه العلامة البيانات المفلترة في جدول شجري قابل للتمرير. كما تتضمن مربع بحث مباشر يقوم بتصفية محتويات الجدول تلقائيًا أثناء كتابة المستخدم.
تشغيل الكود
يُشغَّل الكود بنفس طريقة تشغيل أي برنامج بايثون عادي، لذا احفظه في ملف بايثون، مثلاً tktest.py، وتأكد من تغيير موقع الملف إلى الموقع الذي نزّلته منه من Kaggle. شغّل الكود كالتالي:
python tktest.py
يجب أن تبدو شاشتك هكذا،

يمكنك التبديل بين علامات التبويب “نظرة عامة” و”تحليلات” و”بيانات” لعرض البيانات بطرق مختلفة. عند تغيير نوع السيارة أو نظام الدفع من القائمة المنسدلة، ستُعرض البيانات بهذا التغيير فورًا. استخدم زر “تطبيق فلتر السنة/السعر” لمشاهدة التغييرات في البيانات عند اختيار سنوات أو نطاقات أسعار مختلفة.
شاشة العرض العام هي أول ما تراه عند ظهور واجهة المستخدم الرسومية. وهي تتكون من أربعة مخططات رئيسية وعروض معلوماتية للإحصائيات أسفل حقول التصفية مباشرةً.
توفر علامة التبويب “التحليل” أربع طرق عرض إضافية للبيانات: خريطة حرارية للارتباط، ومخطط سعر السيارة حسب العلامة التجارية، ومخطط استهلاك الوقود الذي يوضح كفاءة مختلف العلامات التجارية والطرازات، ومخطط تقييم لستة معايير مختلفة. في كل من مخطط سعر السيارة حسب العلامة التجارية ومخطط استهلاك الوقود مقابل السعر في علامة التبويب “نظرة عامة”، يمكنك النقر على أي نقطة في المخطط لمعرفة العلامة التجارية والطراز المقصودين. إليك شكل مخطط استهلاك الوقود الذي يوضح كفاءة مختلف العلامات التجارية عند مقارنة معدلات استهلاك الوقود داخل المدينة وعلى الطرق السريعة.


وأخيرًا، لدينا علامة تبويب البيانات. وهي عبارة عن تمثيل جدولي للبيانات الأساسية في شكل صفوف وأعمدة. وكما هو الحال مع جميع المخططات المعروضة، يتغير هذا الناتج عند تصفية البيانات.
لرؤيته عمليًا، قمت أولاً بالنقر على علامة التبويب “نظرة عامة” وقمت بتغيير معلمات الإدخال إلى،
Make: BMW
Drivetrain: All-wheel Drive
Price: 2300.0 to 449996.0
Year: 2022
ثم نقرت على علامة تبويب البيانات وحصلت على هذه النتيجة.

تُقدّم هذه المقالة دليلاً شاملاً لاستخدام Tkinter، مكتبة واجهة المستخدم الرسومية الأصلية المدمجة في لغة بايثون، لإنشاء تطبيقات سطح مكتب حديثة تعتمد على البيانات. إنها أداة متينة وخفيفة الوزن وما زالت ذات أهمية، وبالاقتران مع مكتبة ttkbootstrap، تُصبح قادرة على إنتاج عروض بيانات ولوحات معلومات عصرية المظهر.
بدأت بتغطية اللبنات الأساسية لأي تطبيق Tkinter، مثل النافذة الجذرية، والأدوات (الأزرار، والملصقات)، ومديري الهندسة للتخطيط.
ثم انتقلت إلى أداة تحليلية كاملة الميزات بواجهة علامات تبويب، وفلاتر ديناميكية تقوم بتحديث جميع العناصر المرئية في الوقت الفعلي، ومخططات قابلة للنقر توفر تجربة مستخدم سريعة الاستجابة واحترافية، مما يقدم حجة قوية لقدرات Tkinter التي تتجاوز الأدوات البسيطة.
اكتشاف المزيد من بايثون العربي
اشترك للحصول على أحدث التدوينات المرسلة إلى بريدك الإلكتروني.