✅ الهيكل: 20 درسًا موسعًا حسب خطتك
✅ اللغة: عربي، مع كود بالإنجليزية (كما هو متعارف عليه)
✅ الجمهور: مبرمجون يعرفون Python ويرغبون في بناء واجهات رسومية تفاعلية عالية الأداء
مقدمة عن PyQtGraph
التثبيت والإعداد
مفهوم GraphicsWindow وPlotWidget
PlotItem و ViewBox
الرسم الخطي Line Plot
plot()
.الرسم التشتتي Scatter Plot
ScatterPlotItem
.رسم الأعمدة Bar Graphs
الصور ImageView
التكبير والتحريك Zoom & Pan
إضافة التعليقات Annotations
التعامل مع الإشارات Signals
الرسم اللحظي (Real-time plotting)
الرسوم ثلاثية الأبعاد
GLViewWidget
.الخرائط اللونية (Color Maps)
Multiple Plots
دمج PyQtGraph في واجهات PyQt/PySide
التصدير والحفظ
مشروع عملي
setData()
بكفاءة.PyQtGraph هي مكتبة بايثون مبنية على PyQt5/PySide6 تُستخدم لرسم البيانات بسرعة وكفاءة، خصوصًا في التطبيقات التي تتطلب: - عرض بيانات في الزمن الحقيقي (real-time) - تفاعلات سريعة (تكبير، تحريك، نقر) - أداء عالٍ مع كميات كبيرة من البيانات
💡 تُستخدم في:
- أنظمة مراقبة المستشعرات
- أدوات التحليل الطيفي
- واجهات تداول العملات
- التجارب العلمية
الميزة | PyQtGraph | Matplotlib | Plotly |
---|---|---|---|
الأداء | ⚡ عالي جدًا | متوسط | متوسط إلى منخفض |
الزمن الحقيقي | ✅ ممتاز | ❌ ضعيف | ⚠️ محدود |
التفاعلية | ✅ مدمجة | ❌ تحتاج إعداد | ✅ جيدة |
التكامل مع GUI | ✅ ممتاز (مع PyQt) | ⚠️ ممكن لكن معقد | ✅ مع Dash |
السهولة | متوسطة | عالية | عالية |
✅ استخدم PyQtGraph عندما:
- تحتاج تحديثات سريعة (مئات الإطارات في الثانية)
- تعمل على تطبيق ديسك توب (Desktop App)
- تعرض بيانات مستشعرات أو إشارات كهربائية
pip install pyqtgraph PySide6
✅ نوصي بـ PySide6 لأنه مفتوح المصدر بالكامل ولا يتطلب ترخيصًا.
import pyqtgraph as pg
from PySide6.QtWidgets import QApplication
import sys
# إنشاء تطبيق Qt
= QApplication(sys.argv)
app
# إنشاء نافذة عرض
= pg.GraphicsLayoutWidget(show=True, title="أول نافذة في PyQtGraph")
win 800, 600)
win.resize(
# عرض النافذة
if __name__ == '__main__':
exec() pg.
✅
show=True
: تُظهر النافذة فور الإنشاء
✅title
: عنوان النافذة
✅pg.exec()
: تبدأ حلقة الأحداث (Event Loop)
الخطأ | السبب | الحل |
---|---|---|
ModuleNotFoundError |
لم تُثبّت المكتبة | pip install pyqtgraph |
النافذة تفتح وتغلق فورًا | لم تُضف pg.exec() |
أضف pg.exec() في النهاية |
لا تظهر أي وظائف | استخدمت import pyqtgraph بدل import pyqtgraph as pg |
استخدم as pg |
PySide6
بدل PyQt5
لتجنب مشاكل الترخيص.if __name__ == '__main__':
لمنع التنفيذ عند الاستيراد.بعد أن أنشأت أول نافذة في PyQtGraph، حان الوقت لفهم البنية الداخلية للرسم.
في PyQtGraph، لا ترسم البيانات مباشرة على النافذة.
بالتالي، ما هي العناصر التي تُستخدم لعرض الرسومات؟
الإجابة تكمن في فهم الفرق بين: - PlotWidget
- GraphicsLayoutWidget
كلاهما يُستخدم لعرض الرسوم، لكن لكل منهما استخداماته وخصائصه.
PlotWidget
— أبسط طريقة لعرض رسمPlotWidget
هو عنصر (Widget) جاهز يمكنك إضافته إلى واجهة PyQt، ويحتوي على: - رسم بياني واحد - محور X وY - أداة تكبير وتحريك مدمجة - إمكانيات تخصيص عالية
import pyqtgraph as pg
from PySide6.QtWidgets import QApplication
import numpy as np
import sys
= QApplication(sys.argv)
app
# إنشاء نافذة تحتوي على PlotWidget
= pg.plot(title="رسم خطي بسيط")
win 'left', 'القيمة')
win.setLabel('bottom', 'الزمن')
win.setLabel(0, 10)
win.setXRange(0, 100)
win.setYRange(
# بيانات بسيطة
= np.arange(10)
x = np.array([10, 20, 25, 30, 50, 60, 75, 80, 90, 100])
y
# رسم البيانات
= win.plot(x, y, pen='b', symbol='o', symbolBrush='r', name='البيانات')
curve
# عرض النافذة
if __name__ == '__main__':
exec() pg.
pg.plot()
: دالة سريعة لإنشاء PlotWidget
وعرض رسم فورًا.setLabel()
: لوضع عناوين للمحاور.setXRange()
, setYRange()
: لتحديد مدى العرض.pen='b'
: خط أزرق.symbol='o'
: نقاط دائرية.symbolBrush='r'
: نقاط حمراء.✅ هذا هو الشكل المثالي للرسومات البسيطة.
GraphicsLayoutWidget
— للرسومات المعقدة متعددة الأجزاءإذا أردت عرض عدة رسومات في نافذة واحدة (مثل: رسمين جنبًا إلى جنب، أو عموديًا)، فاستخدم GraphicsLayoutWidget
.
import pyqtgraph as pg
from PySide6.QtWidgets import QApplication
import numpy as np
import sys
= QApplication(sys.argv)
app
# إنشاء نافذة متعددة الأجزاء
= pg.GraphicsLayoutWidget(show=True, title="رسمان في نافذة واحدة")
win 1000, 600)
win.resize(
# إنشاء أول رسم (صف 0، عمود 0)
= win.addPlot(row=0, col=0, title="الرسم 1: جيب التمام")
p1 = np.linspace(0, 4*np.pi, 100)
x = np.cos(x)
y1 ='g')
p1.plot(x, y1, pen
# إنشاء ثاني رسم (صف 0، عمود 1)
= win.addPlot(row=0, col=1, title="الرسم 2: الجيب")
p2 = np.sin(x)
y2 ='m')
p2.plot(x, y2, pen
# إضافة شبكة للرسم الثاني
=True, y=True, alpha=0.5)
p2.showGrid(x
if __name__ == '__main__':
exec() pg.
addPlot(row, col)
: لإضافة رسم في موقع معين.showGrid()
: لعرض شبكة خلفية.✅ مثالي لواجهات تتطلب عرض بيانات متعددة (مثل: مراقبة مستشعرات متعددة).
PlotWidget
و GraphicsLayoutWidget
الميزة | PlotWidget |
GraphicsLayoutWidget |
---|---|---|
الغرض | رسم واحد بسيط | عدة رسومات في نافذة واحدة |
السهولة | عالية (pg.plot() ) |
متوسطة (يتطلب addPlot ) |
التحكم في التخطيط | محدود | كامل (صفوف وأعمدة) |
الأداء | عالي | عالي |
الاستخدام الشائع | تطبيقات بسيطة | واجهات متقدمة |
الحالة | الأداة الموصى بها |
---|---|
عرض رسم واحد بسيط | ✅ PlotWidget |
رسم بيانات زمنية لحظية | ✅ PlotWidget |
عرض 4 رسومات (2×2) | ✅ GraphicsLayoutWidget |
بناء واجهة معقدة مع أدوات | ✅ GraphicsLayoutWidget |
بدء تعلم PyQtGraph | ✅ PlotWidget |
الخطأ | السبب | الحل |
---|---|---|
NameError: name 'pg' is not defined |
لم تستورد pyqtgraph as pg |
تأكد من import pyqtgraph as pg |
لا تظهر النافذة | نسيت pg.exec() |
أضف pg.exec() في النهاية |
الرسم فارغ | البيانات ليست من نوع numpy array أو list |
تأكد من صحة البيانات |
addPlot() لا يعمل |
استخدمت pg.plot() بدل GraphicsLayoutWidget |
استخدم win = pg.GraphicsLayoutWidget() |
PlotWidget
يعرض منحنى تربيعي: y = x²
.t
).GraphicsLayoutWidget
به 3 رسومات: في صف واحد.# فرض بيانات مستشعرات (درجة حرارة، رطوبة، ضغط)
= np.random.rand(50) * 30 + 20 # 20-50°C
temp = np.random.rand(50) * 30 + 40 # 40-70%
humi = np.random.rand(50) * 10 + 980 # 980-990 hPa
pres
# إنشاء النافذة
= pg.GraphicsLayoutWidget(show=True, title="مراقبة المستشعرات")
win 1200, 400)
win.resize(
# درجة الحرارة
= win.addPlot(row=0, col=0, title="درجة الحرارة")
p1 ='r', symbol='o', symbolBrush='r')
p1.plot(temp, pen
# الرطوبة
= win.addPlot(row=0, col=1, title="الرطوبة")
p2 ='b', symbol='t', symbolBrush='b')
p2.plot(humi, pen
# الضغط
= win.addPlot(row=0, col=2, title="الضغط")
p3 ='g', symbol='s', symbolBrush='g')
p3.plot(pres, pen
print("✅ واجهة المراقبة جاهزة للتشغيل اللحظي!")
✅ هذا النوع من الواجهات يُستخدم في الأنظمة الصناعية.
PlotWidget
إذا كنت مبتدئًا.GraphicsLayoutWidget
للتطبيقات المعقدة.pg.plot()
للتجريب السريع.win
و p1
, p2
كمتغيرات للوصول إليها لاحقًا.win.resize()
لضبط حجم النافذة.pg.plot()
و GraphicsLayoutWidget
؟showGrid()
؟pg.exec()
في النهاية؟الأداة | الوظيفة |
---|---|
pg.plot() |
إنشاء PlotWidget سريع |
PlotWidget |
عرض رسم واحد |
GraphicsLayoutWidget |
عرض عدة رسومات |
addPlot(row, col) |
إضافة رسم في موقع معين |
setLabels() |
تسمية المحاور |
showGrid() |
عرض شبكة |
بعد أن تعلمت كيفية إنشاء نافذة عرض (PlotWidget
أو GraphicsLayoutWidget
)، حان الوقت لفهم العناصر الداخلية التي تُكوّن الرسم البياني نفسه.
في PyQtGraph، لا يتم رسم البيانات مباشرة على النافذة، بل عبر طبقة من العناصر تتحكم في كل شيء: - المحاور (X و Y) - العنوان - الشبكة - التكبير والتحريك - حدود العرض
هذان العنصران الرئيسيان هما: - PlotItem
- ViewBox
فهمهما يمنحك تحكمًا دقيقًا في شكل وسلوك الرسم.
PlotItem
— وحدة التحكم في الرسمPlotItem
؟PlotItem
هو الكائن الذي يحتوي على: - المحاور (Axis) - العنوان (Title) - الشبكة (Grid) - جميع الخطوط والنقاط (الـ Items) - الإعدادات العامة للرسم
💡 اعتبر أن
PlotItem
هو “الرسم البياني” نفسه، بينماPlotWidget
هو “الإطار” الذي يحتويه.
PlotItem
وتخصيصهimport pyqtgraph as pg
from PySide6.QtWidgets import QApplication
import numpy as np
import sys
= QApplication(sys.argv)
app
# إنشاء نافذة باستخدام PlotWidget
= pg.plot(title="تخصيص PlotItem")
win 800, 600)
win.resize(
# 1. الوصول إلى PlotItem
= win.getPlotItem()
plot_item
# 2. تخصيص المحاور
'left', 'الدرجة (°C)')
plot_item.setLabel('bottom', 'الزمن (ثانية)')
plot_item.setLabel(
# 3. تعيين عنوان
'بيانات مستشعر درجة الحرارة')
plot_item.setTitle(
# 4. إضافة شبكة
=True, y=True, alpha=0.3)
plot_item.showGrid(x
# 5. تحديد مدى العرض
0, 10)
plot_item.setXRange(15, 35)
plot_item.setYRange(
# 6. إضافة بيانات
= np.linspace(0, 10, 100)
x = 25 + 5 * np.sin(x) + np.random.normal(0, 0.5, 100)
y ='b', name='الدرجة')
plot_item.plot(x, y, pen
if __name__ == '__main__':
exec() pg.
getPlotItem()
: يُرجع كائن PlotItem
الموجود داخل PlotWidget
.setLabel()
: لوضع تسميات للمحاور.showGrid()
: لعرض شبكة خلفية.setXRange()
, setYRange()
: لضبط حدود العرض.plot()
: لإضافة بيانات إلى PlotItem
.✅ هذا هو الشكل المثالي للتحكم الكامل في الرسم.
ViewBox
— قلب التفاعليةViewBox
؟ViewBox
هو العنصر المسؤول عن: - التكبير (Zoom) - التحريك (Pan) - تحديد منطقة العرض - التحكم في سلوك الفأرة
💡 بدون
ViewBox
، لا يمكن التفاعل مع الرسم.
ViewBox
# بعد الحصول على plot_item
= plot_item.getViewBox()
view_box
# تعطيل التفاعلية (لإيقاف التكبير والتحريك)
# view_box.setMouseEnabled(False, False)
# تمكين التكبير فقط على المحور X
# view_box.setMouseEnabled(x=True, y=False)
# تغيير لون خلفية ViewBox
'w') # أبيض view_box.setBackgroundColor(
✅ مفيد في التطبيقات التي تحتاج تحكمًا دقيقًا في التفاعل.
def on_pan():
print("تم التحريك!")
# يمكنك هنا تحديث عناصر أخرى في الواجهة
# ربط الحدث
connect(on_pan) view_box.sigXRangeChanged.
✅
sigXRangeChanged
: يُطلق عند التكبير أو التحريك.
PlotWidget
و PlotItem
و ViewBox
العنصر | الوظيفة | من يتحكم فيه؟ |
---|---|---|
PlotWidget |
الحاوية (Widget) التي تُضاف إلى واجهة PyQt | أنت (كـ QWidget ) |
PlotItem |
وحدة التحكم في الرسم (المحاور، العنوان، الشبكة) | PlotWidget |
ViewBox |
وحدة التفاعلية (التكبير، التحريك) | PlotItem |
🎯 التسلسل الهرمي:
PlotWidget └── PlotItem └── ViewBox └── البيانات (الخطوط، النقاط...)
الخطأ | السبب | الحل |
---|---|---|
AttributeError: 'PlotWidget' object has no attribute 'setLabel' |
حاولت استخدام setLabel على PlotWidget مباشرة |
استخدم getPlotItem().setLabel() |
لا يعمل التكبير | setMouseEnabled(False) |
أعد تمكين الفأرة |
لا تظهر الشبكة | نسيت showGrid() |
أضف showGrid(x=True, y=True) |
sigXRangeChanged لا يعمل |
لم تُنشئ ViewBox بشكل صحيح |
تأكد من getViewBox() |
PlotWidget
، ثم استخدم getPlotItem()
لتغيير لون خلفية ViewBox
إلى رمادي فاتح.# فرض إشارة كهربائية (مثل ECG)
= np.linspace(0, 5, 1000)
t = 0.5 * np.sin(2*np.pi*5*t) + 0.1 * np.random.randn(1000)
signal
# إنشاء النافذة
= pg.plot()
win = win.getPlotItem()
plot_item = plot_item.getViewBox()
view_box
# التخصيص
'left', 'الجهد (V)')
plot_item.setLabel('bottom', 'الزمن (ثانية)')
plot_item.setLabel('تحليل إشارة كهربائية')
plot_item.setTitle(=True, y=True, alpha=0.5)
plot_item.showGrid(x'k') # خلفية سوداء للمحترفين
view_box.setBackgroundColor(
# رسم الإشارة
='y')
plot_item.plot(t, signal, pen
# ربط حدث
def on_zoom():
print(f"مدى X الحالي: {view_box.viewRange()[0]}")
connect(on_zoom)
view_box.sigXRangeChanged.
print("📊 الواجهة جاهزة لتحليل الإشارات!")
✅ هذا النوع من الواجهات يُستخدم في الأجهزة الطبية والصناعية.
getPlotItem()
دائمًا عند الحاجة لتخصيص متقدم.getViewBox()
للتحكم في التفاعلية.plot_item
و view_box
كمتغيرات للوصول السريع.setMouseEnabled()
لتعطيل الحركة عند الحاجة (مثلاً: أثناء التسجيل).sigXRangeChanged
لتحديث عناصر أخرى في الواجهة (مثل: عرض الوقت الحالي).PlotWidget
و PlotItem
؟ViewBox
؟showGrid()
؟sigXRangeChanged
؟العنصر | الوظيفة |
---|---|
PlotWidget |
الحاوية الأساسية |
getPlotItem() |
للوصول إلى إعدادات الرسم |
setLabel() |
تسمية المحاور |
showGrid() |
عرض شبكة |
setXRange() |
ضبط مدى العرض |
getViewBox() |
للتحكم في التفاعلية |
setMouseEnabled() |
تمكين/تعطيل التكبير |
sigXRangeChanged |
ربط حدث التكبير |
الرسم الخطي (Line Plot) هو أكثر أنواع الرسوم شيوعًا في التحليل العلمي والهندسي.
يُستخدم لتمثيل: - كيف تتغير قيمة ما مع الزمن (مثل: سعر السهم، درجة الحرارة) - العلاقة بين متغيرين (مثل: السعر والطلب) - الاتجاهات والأنماط في البيانات
في هذا الدرس، ستتعلم كيفية إنشاء رسم خطي باستخدام PyQtGraph، وتخصيصه بالكامل: الألوان، الأنماط، النقاط، والأساطير.
import pyqtgraph as pg
from PySide6.QtWidgets import QApplication
import numpy as np
import sys
= QApplication(sys.argv)
app
# إنشاء نافذة رسم
= pg.plot(title="رسم دالة cos(x)")
win
# إنشاء البيانات
= np.linspace(0, 4*np.pi, 100) # 100 نقطة من 0 إلى 4π
x = np.cos(x)
y
# رسم البيانات
= win.plot(x, y)
curve
if __name__ == '__main__':
exec() pg.
np.linspace()
: لتوليد نقاط متساوية.win.plot(x, y)
: لرسم الخط.✅ هذا هو الشكل الأساسي للرسم الخطي.
يمكنك تغيير لون، سمك، ونمط الخط باستخدام معامل pen
.
= win.plot(x, y, pen=pg.mkPen(color='g', width=3, style=pg.QtCore.Qt.DashLine)) curve
pen
الشائعةالخيار | القيمة | الوصف |
---|---|---|
color |
'r' , 'g' , 'b' , '#FF0000' |
لون الخط |
width |
2 , 5 |
سمك الخط (بكسل) |
style |
Qt.SolidLine , Qt.DashLine , Qt.DotLine |
نمط الخط |
لجعل الرسم أكثر وضوحًا، أضف نقاطًا على كل قيمة.
= win.plot(x, y,
curve ='b',
pen='o', # شكل النقطة: 'o', 's', 't', 'd'
symbol=8, # حجم النقطة
symbolSize='r', # لون ملء النقطة
symbolBrush='w') # لون الحدودة symbolPen
الرمز | الشكل |
---|---|
'o' |
دائرة |
's' |
مربع |
't' |
مثلث |
'd' |
الماس |
'+' |
علامة زائد |
لجعل الرسم مفهومًا، أضف تسميات.
'left', 'القيمة (V)')
win.setLabel('bottom', 'الزمن (ثانية)')
win.setLabel('تحليل إشارة جيب التمام') win.setTitle(
لتسهيل القراءة.
=True, y=True, alpha=0.5) win.showGrid(x
alpha
: درجة الشفافية (من 0 إلى 1)مفيد عند رسم أكثر من منحنى.
= pg.GraphicsLayoutWidget(show=True, title="دالتا الجيب والجيب تمامًا")
win = win.addPlot()
p1
# بيانات
= np.linspace(0, 4*np.pi, 100)
x = np.cos(x)
y1 = np.sin(x)
y2
# رسم cos(x)
='b', name='cos(x)', symbol='o', symbolSize=5)
p1.plot(x, y1, pen
# رسم sin(x)
='r', name='sin(x)', symbol='t', symbolSize=5)
p1.plot(x, y2, pen
True, True, 0.3)
p1.showGrid('left', 'القيمة')
p1.setLabel('bottom', 'الزمن') p1.setLabel(
✅
name=
هو ما يظهر في الأسطورة.
الخطأ | السبب | الحل |
---|---|---|
لا يظهر الخط | pen=None أو لون شفاف |
تأكد من pen='color' |
النقاط لا تظهر | نسيت symbol |
أضف symbol='o' |
الأسطورة لا تظهر | نسيت name= |
أضف name='الاسم' |
الشبكة لا تظهر | نسيت showGrid() |
أضف showGrid(True, True) |
y = x²
من -10 إلى 10.import pyqtgraph as pg
from PySide6.QtWidgets import QApplication
import numpy as np
import sys
= QApplication(sys.argv)
app
# نافذة عرض السعر
= pg.plot(title="سعر السهم - التحديث اللحظي")
win 'left', 'السعر (ريال)')
win.setLabel('bottom', 'الزمن')
win.setLabel(=True, alpha=0.3)
win.showGrid(y
# بيانات وهمية لسعر السهم
= np.arange(100)
time = 100 + np.cumsum(np.random.randn(100) * 0.5) # مشي عشوائي
price
# رسم السعر
= win.plot(time, price, pen='c', symbol='o', symbolSize=6, symbolBrush='m', name='السعر')
curve
print("📈 واجهة عرض السعر جاهزة!")
if __name__ == '__main__':
exec() pg.
✅ هذا الشكل يُستخدم في لوحات التداول.
pg.mkPen()
لصنع ألوان متقدمة.alpha
في symbolBrush
لجعل النقاط شفافة.curve
إذا كنت ستحديثه لاحقًا.GraphicsLayoutWidget
إذا كنت سترسم أكثر من رسم.name=
فقط عند الحاجة إلى أسطورة.symbolBrush
و symbolPen
؟name=
في plot()
؟العنصر | الكود |
---|---|
رسم خط | plot(x, y) |
تغيير لون الخط | pen='r' أو pen=pg.mkPen(...) |
إضافة نقاط | symbol='o' |
تغيير حجم النقاط | symbolSize=8 |
ألوان النقاط | symbolBrush='b' |
تسمية المحاور | setLabel('left', '...') |
الشبكة | showGrid(x=True, y=True) |
الأسطورة | name='الاسم' |
الرسم التشتتي (Scatter Plot) هو أداة قوية لتمثيل العلاقة بين متغيرين عددين.
يُستخدم لفهم: - هل هناك ارتباط بين السعر والطلب؟ - كيف تتوزع النقاط في الفضاء (مثل: بيانات العملاء حسب العمر والدخل)؟ - اكتشاف القيم الشاذة (Outliers)
في هذا الدرس، ستتعلم كيفية إنشاء رسم تشتتي باستخدام ScatterPlotItem
، وتخصيصه بالكامل: الألوان، الأحجام، والتفاعل.
ScatterPlotItem
— الطريقة الأساسيةimport pyqtgraph as pg
from PySide6.QtWidgets import QApplication
import numpy as np
import sys
= QApplication(sys.argv)
app
# إنشاء نافذة
= pg.plot(title="رسم تشتتي: العلاقة بين X وY")
win 'bottom', 'X')
win.setLabel('left', 'Y')
win.setLabel(True, True, 0.3)
win.showGrid(
# بيانات وهمية
= np.random.normal(size=100)
x = x * 0.5 + np.random.normal(size=100) * 0.3 # علاقة خطية
y
# إنشاء عنصر النقاط
= pg.ScatterPlotItem(x=x, y=y, pen=None, brush='b', size=10)
scatter
# إضافة العنصر إلى النافذة
win.addItem(scatter)
if __name__ == '__main__':
exec() pg.
pg.ScatterPlotItem()
: يُنشئ مجموعة من النقاط.pen=None
: لا حدود للنقاط.brush='b'
: لون ملء النقطة (أزرق).size=10
: حجم النقطة.win.addItem(scatter)
: لإضافة العنصر إلى الرسم.✅ مثالي للبيانات ذات الألوان والمقاييس المتغيرة.
يمكنك تغيير شكل، حجم، ولون كل نقطة.
# بيانات
= [1, 2, 3, 4, 5]
x = [2, 5, 3, 8, 7]
y = ['r', 'g', 'b', 'c', 'm'] # أحمر، أخضر، أزرق، سماوي، أرجواني
colors = [20, 30, 40, 50, 60]
sizes
# إنشاء النقاط
= []
spots for i in range(len(x)):
spots.append({'pos': (x[i], y[i]),
'size': sizes[i],
'brush': colors[i]
})
= pg.ScatterPlotItem(spots=spots)
scatter win.addItem(scatter)
✅ يمكنك استخدام قاموس لكل نقطة لتحكم دقيق.
plot()
لرسم تشتتي (طريقة مختصرة)إذا كنت لا تحتاج تحكمًا دقيقًا، يمكنك استخدام plot()
.
# طريقة أسرع
=None, symbol='o', symbolBrush='r', symbolSize=10) win.plot(x, y, pen
✅ نفس النتيجة، لكن أقل مرونة.
لعرض الاتجاه مع التوزيع.
# رسم خط + نقاط
= win.plot(x, y, pen='g') # خط أخضر
curve = pg.ScatterPlotItem(x=x, y=y, brush='r', size=8)
scatter win.addItem(scatter)
✅ مفيد في تتبع المسارات أو الإشارات.
لحل مشكلة التداخل (Overplotting).
# شفافية 50%
= pg.mkBrush(color=(255, 0, 0, 128)) # RGBA (الأخير هو alpha)
brush = pg.ScatterPlotItem(x=x, y=y, brush=brush, size=10) scatter
✅
128
= 50% شفافية (من 0 إلى 255)
الخطأ | السبب | الحل |
---|---|---|
لا تظهر النقاط | pen=None وbrush=None |
تأكد من تعيين brush |
الحجم صغير جدًا | size=1 |
زد قيمة size |
الألوان لا تعمل | تمرير قائمة مباشرة | استخدم spots أو mkBrush |
addItem() لا يعمل |
استخدمت plot() بدل addItem |
تأكد من استخدام addItem مع ScatterPlotItem |
size = y * 5
).# بيانات وهمية للعملاء
42)
np.random.seed(= np.random.randint(18, 70, 100)
age = 20000 + age * 1000 + np.random.randn(100) * 5000
income
# إنشاء النافذة
= pg.GraphicsLayoutWidget(show=True, title="تحليل العملاء", size=(800, 600))
win = win.addPlot(title="العمر مقابل الدخل")
p1 'bottom', 'العمر (سنة)')
p1.setLabel('left', 'الدخل (ريال)')
p1.setLabel(True, True, 0.3)
p1.showGrid(
# تحديد نقاط العملاء المميزين (دخل عالي)
= ['r' if inc > 80000 else 'b' for inc in income]
colors = [15 if inc > 80000 else 8 for inc in income]
sizes
= [{'pos': (age[i], income[i]), 'size': sizes[i], 'brush': colors[i]} for i in range(len(age))]
spots = pg.ScatterPlotItem(spots=spots)
scatter
p1.addItem(scatter)
# إضافة نص توضيحي
= pg.TextItem(text="العملاء المميزون (أحمر)", color='r', anchor=(0, 1))
text 30, 120000)
text.setPos(
p1.addItem(text)
print("📊 تحليل العملاء جاهز للعرض!")
✅ هذا النوع من التحليل يُستخدم في التسويق والتجزئة.
ScatterPlotItem
عندما تحتاج تحكمًا دقيقًا في كل نقطة.spots
لجعل كل نقطة فريدة.plot()
و ScatterPlotItem
للحصول على خطوط ونقاط.addItem()
وليس plot()
عند استخدام ScatterPlotItem
.plot()
و ScatterPlotItem
؟alpha
في mkBrush
؟العنصر | الكود |
---|---|
ScatterPlotItem() |
إنشاء نقاط |
spots |
قائمة بالخصائص لكل نقطة |
pos |
موقع النقطة (x, y) |
size |
حجم النقطة |
brush |
لون ملء النقطة |
pen |
لون الحدودة |
alpha |
الشفافية (في RGBA) |
addItem() |
إضافة العنصر للرسم |
رسم الأعمدة (Bar Graph) هو الطريقة المثالية لتمثيل البيانات الفئوية (Categorical Data) ومقارنتها.
يُستخدم لعرض: - مبيعات كل منتج - عدد الموظفين في كل قسم - توزيع الدرجات بين الطلاب - نتائج الاستبيانات
في هذا الدرس، ستتعلم كيفية إنشاء رسم أعمدة باستخدام PyQtGraph، وتخصيصه بالكامل: الألوان، التسميات، والتباين.
BarGraphItem
— الطريقة الأساسيةimport pyqtgraph as pg
from PySide6.QtWidgets import QApplication
import numpy as np
import sys
= QApplication(sys.argv)
app
# إنشاء نافذة
= pg.plot(title="مبيعات المنتجات")
win 'bottom', 'المنتج')
win.setLabel('left', 'الكمية')
win.setLabel(=True, alpha=0.3)
win.showGrid(y
# بيانات المبيعات
= ['تفاح', 'موز', 'حليب', 'لابتوب']
products = [50, 30, 40, 5]
sales
# تحويل الأسماء إلى مواقع عددية
= np.arange(len(products))
x
# إنشاء أعمدة
= pg.BarGraphItem(x=x, height=sales, width=0.6, brush='g')
bars
# إضافة الأعمدة إلى النافذة
win.addItem(bars)
# تعديل تسميات محور X
= win.getAxis('bottom')
ax for i in range(len(products))]])
ax.setTicks([[(i, products[i])
if __name__ == '__main__':
exec() pg.
BarGraphItem(x, height, width, brush)
: يُنشئ أعمدة.x
: المواقع الأفقية (أرقام).height
: ارتفاع العمود (القيمة).width
: عرض العمود.brush
: لون العمود.setTicks()
: لتغيير تسميات المحور X إلى أسماء.✅ هذا هو الشكل الأساسي لرسم الأعمدة.
يمكنك جعل كل عمود بلون مختلف.
# ألوان لكل عمود
= ['r', 'y', 'b', 'm'] # أحمر، أصفر، أزرق، أرجواني
colors
# إنشاء الأعمدة
= pg.BarGraphItem(x=x, height=sales, width=0.6)
bars
# تعيين الألوان فورًا
=colors)
bars.setOpts(brushes
win.addItem(bars)
✅ استخدم
brushes
(جمع) لتحديد لون لكل عمود.
لجعل الرسم أكثر وضوحًا.
from pyqtgraph import TextItem
# بعد إنشاء الأعمدة
for i in range(len(products)):
= TextItem(text=str(sales[i]), color='k', anchor=(0.5, 1.2))
text
text.setPos(x[i], sales[i]) win.addItem(text)
✅
anchor=(0.5, 1.2)
: لمركز النص فوق العمود.
لعرض بيانات متعددة (مثل: مبيعات 2023 و2024).
# بيانات
= [50, 30, 40, 5]
sales_2023 = [55, 35, 42, 8]
sales_2024
# تحديد عرض المجموعة
= 0.3
width
# إنشاء الأعمدة
= pg.BarGraphItem(x=x - width/2, height=sales_2023, width=width, brush='b', name='2023')
bars_2023 = pg.BarGraphItem(x=x + width/2, height=sales_2024, width=width, brush='r', name='2024')
bars_2024
win.addItem(bars_2023)
win.addItem(bars_2024)
# إضافة أسطورة
= win.addLegend()
legend '2023')
legend.addItem(bars_2023, '2024') legend.addItem(bars_2024,
✅
x ± width/2
: لوضع الأعمدة جنبًا إلى جنب.
لعرض المجموع الكلي.
# القيم المكدسة
= sales_2023
y1 = [a + b for a, b in zip(sales_2023, sales_2024)]
y2
= pg.BarGraphItem(x=x, height=y1, width=0.6, brush='b', name='2023')
bars_2023 = pg.BarGraphItem(x=x, y=y1, height=sales_2024, width=0.6, brush='r', name='2024')
bars_2024
win.addItem(bars_2023) win.addItem(bars_2024)
✅
y=y1
: لبدء العمود الجديد من قمة العمود الأول.
الخطأ | السبب | الحل |
---|---|---|
لا تظهر الأعمدة | width=0 أو height=0 |
تأكد من القيم |
التسميات لا تظهر | نسيت setTicks() |
أضف setTicks() للمحور |
الألوان لا تعمل | استخدمت brush بدل brushes |
استخدم setOpts(brushes=...) |
addItem() لا يعمل |
لم تُنشئ BarGraphItem أولًا |
تأكد من pg.BarGraphItem() |
# بيانات وهمية
= ['موز', 'تفاح', 'حليب', 'خبز', 'لبن']
products = [120, 150, 80, 100, 90]
jan_sales = [130, 140, 85, 110, 95]
feb_sales
= np.arange(len(products))
x = 0.4
width
= pg.GraphicsLayoutWidget(show=True, title="مبيعات يناير وفبراير", size=(900, 600))
win = win.addPlot()
p1
# الأعمدة
= pg.BarGraphItem(x=x - width/2, height=jan_sales, width=width, brush='g', name='يناير')
bars_jan = pg.BarGraphItem(x=x + width/2, height=feb_sales, width=width, brush='b', name='فبراير')
bars_feb
p1.addItem(bars_jan)
p1.addItem(bars_feb)
# التسميات
= p1.getAxis('bottom')
ax for i in range(len(products))]])
ax.setTicks([[(i, products[i])
'left', 'الكمية')
p1.setLabel('bottom', 'المنتج')
p1.setLabel('مقارنة مبيعات شهرين')
p1.setTitle(=True, alpha=0.3)
p1.showGrid(y
# أسطورة
p1.addLegend()print("📊 تقرير المبيعات جاهز للعرض!")
✅ هذا النوع من التقارير يُستخدم في الإدارة.
BarGraphItem
فقط للرسمات الثابتة.plot()
مع stepMode=True
للتحديث السريع.TextItem
لإضافة تسميات.addLegend()
للرسومات المتعددة.setTicks()
لتخصيص تسميات المحور.brush
و brushes
؟العنصر | الكود |
---|---|
BarGraphItem() |
إنشاء أعمدة |
x , height |
الموقع والارتفاع |
width |
عرض العمود |
brush |
لون العمود |
brushes |
ألوان متعددة |
setTicks() |
تسميات المحور |
TextItem() |
نص فوق العمود |
addLegend() |
أسطورة |
رغم أن PyQtGraph تُعرف بالرسوم البيانية، إلا أنها توفر أداة قوية جدًا لعرض الصور والمصفوفات الثنائية والثلاثية الأبعاد.
تُستخدم في: - تطبيقات التصوير الطبي (مثل: عرض صور الأشعة) - تحليل الصور الحرارية - معالجة الإشارات (Spectrograms) - عرض بيانات المصفوفات (مثل: نتائج التجارب)
العنصر الرئيسي لذلك هو ImageView
— وهو أكثر من مجرد عارض صور، بل يوفر: - التكبير والتحريك - التحكم في السطوع والتباين - اختيار الخرائط اللونية (Colormaps) - قراءة قيمة البكسل عند النقر
ImageView
— العارض المتكاملimport pyqtgraph as pg
from PySide6.QtWidgets import QApplication
import numpy as np
import sys
= QApplication(sys.argv)
app
# إنشاء ImageView
= pg.ImageView()
view
# إنشاء صورة وهمية (مصفوفة 2D)
# مثل: صورة حرارية أو بيانات تجربة
= np.random.normal(size=(100, 100)) * 50 + 100 # قيم من 50 إلى 150
image_data
# عرض الصورة
view.setImage(image_data)
# تعيين عنوان
"عرض صورة باستخدام ImageView")
view.setWindowTitle(
# عرض النافذة
view.show()
if __name__ == '__main__':
exec() pg.
pg.ImageView()
: يُنشئ نافذة عرض صور متكاملة.setImage(data)
: يعرض المصفوفة كصورة.ImageView
يحتوي كل شيء.✅ الناتج: نافذة بها صورة، شريط تمرير للسطوع، وشريط جانبي للتحكم.
ImageView
يُظهر شريطين جانبيين للتحكم في: - الحد الأدنى (Black Level) - الحد الأعلى (White Level)
# بعد إنشاء view
=(50, 150)) view.setImage(image_data, levels
✅
levels=(min, max)
: يُعيّن أي قيمة أقل منmin
تكون سوداء، وأي قيمة أكبر منmax
تكون بيضاء.
يمكنك تغيير طريقة عرض القيم باستخدام خريطة لونية.
import pyqtgraph.colormap as cm
# قائمة بالخرائط الشهيرة: 'viridis', 'plasma', 'hot', 'cool', 'gray', 'jet'
= pg.ImageView()
view =(50, 150), colorMap=cm.get('hot'))
view.setImage(image_data, levels"خريطة لونية: hot")
view.setWindowTitle( view.show()
✅
cm.get('hot')
: يُنشئ خريطة لونية حمراء-صفراء.
يمكن لـ ImageView
عرض سلسلة من الصور (مثل: فيديو أو طبقات متعددة).
# إنشاء بيانات ثلاثية الأبعاد (3 صور حجم 50x50)
= np.random.rand(3, 50, 50)
video_data
# عرض الفيديو
= pg.ImageView()
view
view.setImage(video_data)"عرض 3 أطر (فيديو قصير)")
view.setWindowTitle( view.show()
✅ تظهر أداة تمرير للتنقل بين الإطارات.
للحصول على قيمة البكسل عند النقر.
def on_click():
= view.imageItem.pos() # موقع الصورة
pos = view.getView()
view_box = view_box.mapSceneToView(view_box.mapToView(pg.QtCore.QPoint(pg.QtGui.QCursor.pos())))
mouse_point print(f"تم النقر عند: X={mouse_point.x():.1f}, Y={mouse_point.y():.1f}")
# ربط الحدث بالنقر الأيمن
0].triggered.connect(on_click) view.viewBox.menu.actions()[
✅ مفيد في التطبيقات التي تحتاج تفاعلًا دقيقًا.
الخطأ | السبب | الحل |
---|---|---|
AttributeError: 'ImageView' has no attribute 'plot' |
حاولت استخدام plot() |
استخدم setImage() |
لا تظهر الصورة | البيانات ليست مصفوفة 2D أو 3D | تأكد من shape |
الخريطة اللونية لا تعمل | لم تُستخدم cm.get() |
استخدم colorMap=cm.get('viridis') |
levels لا تؤثر |
القيم خارج النطاق | تأكد من نطاق البيانات |
hot
.# فرض صورة أشعة (مصفوفة 2D)
42)
np.random.seed(= np.random.normal(128, 20, (200, 200))
xray
# إضافة "عيب" صغير (مثل كتلة)
80:100, 90:110] += 50
xray[
# عرض الصورة
= pg.ImageView()
view =(50, 200), colorMap=cm.get('gray'))
view.setImage(xray, levels"تحليل صورة أشعة")
view.setWindowTitle(
view.show()
print("🩻 واجهة تحليل الأشعة جاهزة!")
✅ هذا النوع من التطبيقات يُستخدم في الأنظمة الطبية.
ImageView
فقط للصور أو المصفوفات.colorMap=cm.get('gray')
للصور الطبية.levels
لتحسين التباين.resize()
.setImage()
وليس addItem()
مع ImageView
.ImageView
و PlotWidget
؟levels
في setImage()
؟العنصر | الكود |
---|---|
ImageView() |
إنشاء عارض صور |
setImage(data) |
عرض الصورة |
levels=(min, max) |
التحكم في السطوع |
colorMap=cm.get('...') |
تطبيق خريطة لونية |
setImage(3D_data) |
عرض فيديو أو طبقات |
viewBox |
للتفاعل مع النقر |
في أي تطبيق لعرض البيانات، من الضروري أن يتمكن المستخدم من: - التكبير (Zoom) على منطقة معينة لرؤية التفاصيل الدقيقة (مثل: تغيرات سعر السهم في دقائق). - التحريك (Pan) عبر البيانات الكبيرة (مثل: عرض ساعة من البيانات على مدى 24 ساعة).
PyQtGraph يوفر هذه الوظائف مدمجة افتراضيًا، لكن فهم آلية عملها يمنحك القدرة على: - تعطيلها عند الحاجة - تخصيص سلوكها - ربطها بأحداث أخرى
بمجرد إنشاء PlotWidget
أو ImageView
، تصبح وظائف التفاعلية مفعلة تلقائيًا: - التكبير: استخدام عجلة الماوس (Scroll) - التحريك: الضغط والتحريك بالماوس (Drag) - إعادة التعيين: الضغط بالزر الأيمن → “Auto-range”
import pyqtgraph as pg
from PySide6.QtWidgets import QApplication
import numpy as np
import sys
= QApplication(sys.argv)
app
# إنشاء نافذة
= pg.plot(title="التكبير والتحريك")
win
# بيانات كبيرة (10,000 نقطة)
= np.linspace(0, 100, 10000)
x = np.sin(x) + 0.1 * np.random.randn(10000)
y
# رسم البيانات
win.plot(x, y)
if __name__ == '__main__':
exec() pg.
✅ يمكنك التكبير بالعجلة، والتحريك بالضغط والتحريك.
ViewBox
كما تعلمت في الدرس 4، ViewBox
هو العنصر المسؤول عن التفاعل.
= win.getPlotItem()
plot_item = plot_item.getViewBox()
view_box
# تعطيل التكبير على المحور الرأسي (Y)، والسماح به على الأفقي (X)
=True, y=False) view_box.setMouseEnabled(x
✅ مفيد في الرسومات الزمنية حيث لا تريد تكبير المحور الرأسي.
False, False) view_box.setMouseEnabled(
✅ مفيد عند بناء واجهة تحكم مخصصة.
يمكنك تكبير/تصغير العرض برمجيًا.
# تحديد منطقة الاهتمام (من x=10 إلى x=20)
10, 20) plot_item.setXRange(
plot_item.autoRange()
✅ يُعيد العرض إلى جميع البيانات.
يمكنك تنفيذ إجراءات عند تغيير منطقة العرض.
def on_range_changed():
= view_box.viewRange()[0] # [min_x, max_x]
range_x print(f"نطاق العرض الجديد: {range_x[0]:.2f} - {range_x[1]:.2f}")
# ربط الحدث
connect(on_range_changed) view_box.sigXRangeChanged.
✅ مفيد لتحديث عناصر واجهة أخرى (مثل: عرض الوقت الحالي).
إذا أردت التحكم بالتحريك عبر لوحة المفاتيح.
from PySide6 import QtCore
def keyPressEvent(event):
if event.key() == QtCore.Qt.Key_Left:
=-1)
view_box.translateBy(xelif event.key() == QtCore.Qt.Key_Right:
=1)
view_box.translateBy(xelif event.key() == QtCore.Qt.Key_Up:
=0.1)
view_box.translateBy(yelif event.key() == QtCore.Qt.Key_Down:
=-0.1)
view_box.translateBy(y
# ربط حدث لوحة المفاتيح
= keyPressEvent win.keyPressEvent
✅
translateBy()
يُحرك العرض.
الخطأ | السبب | الحل |
---|---|---|
لا يعمل التكبير | setMouseEnabled(False) |
أعد تمكين الفأرة |
التحريك بطيء | بيانات كبيرة جدًا | استخدم setDownsampling() |
sigXRangeChanged لا يعمل |
لم يتم ربط الحدث بشكل صحيح | تأكد من connect() |
التكبير لا يعيد التعيين | لم تُستخدم autoRange() |
استخدم plot_item.autoRange() |
# فرض إشارة حية (مثل: قياس ضغط دم)
= np.linspace(0, 60, 6000) # 60 ثانية
t = 120 + 20 * np.sin(2*np.pi*0.5*t) + np.random.normal(0, 5, 6000)
signal
= pg.plot(title="مراقبة الإشارة الحية")
win = win.plot(t, signal, pen='y')
curve
= win.getPlotItem()
plot_item = plot_item.getViewBox()
view_box
# تعطيل التكبير على Y (لأن المقياس معروف)
=True, y=False)
view_box.setMouseEnabled(x
# ربط حدث
def update_stats():
= view_box.viewRange()[0]
range_x = signal[(t >= range_x[0]) & (t <= range_x[1])]
visible_data print(f"المتوسط في المنطقة: {np.mean(visible_data):.1f}")
connect(update_stats)
view_box.sigXRangeChanged.
print("🔍 واجهة التحليل جاهزة للتكبير والتحليل!")
✅ هذا النوع من الواجهات يُستخدم في التطبيقات الطبية.
setMouseEnabled()
للتحكم في التفاعل.autoRange()
لإعادة العرض.sigXRangeChanged
لتحديث العناصر الأخرى.translateBy()
للتحريك البرمجي.setDownsampling(True)
للبيانات الكبيرة.autoRange()
؟الوظيفة | الكود |
---|---|
التكبير/التحريك | مدمج افتراضيًا |
تعطيل التفاعل | setMouseEnabled(False, False) |
التكبير على X فقط | setMouseEnabled(x=True, y=False) |
التكبير البرمجي | setXRange(min, max) |
إعادة العرض | autoRange() |
ربط حدث التكبير | sigXRangeChanged.connect(func) |
التحريك بالكود | translateBy(x, y) |
الرسم البياني الجيد لا يُكتفى بعرض البيانات، بل يجب أن يُوجه المستخدم إلى النتائج المهمة.
التعليقات (Annotations) هي أدوات تسمح لك بإضافة: - نصوص توضيحية (“أعلى سعر في الشهر”) - أسهم تشير إلى أحداث معينة (“انقطاع التيار الكهربائي”) - مربعات نصية أو أشرطة لتظليل مناطق معينة
في هذا الدرس، ستتعلم كيفية استخدام عناصر مثل TextItem
و ArrowItem
و InfiniteLine
لجعل رسوماتك أكثر وضوحًا واحترافية.
TextItem
العنصر TextItem
يُستخدم لإضافة نص في موقع محدد على الرسم.
import pyqtgraph as pg
from PySide6.QtWidgets import QApplication
import numpy as np
import sys
= QApplication(sys.argv)
app
# إنشاء نافذة
= pg.plot(title="إضافة نص توضيحي")
win = np.linspace(0, 4*np.pi, 100)
x = np.sin(x)
y ='b')
win.plot(x, y, pen
# إنشاء نص
= pg.TextItem(text="ذروة الموجة", color='r', anchor=(0.5, 1.2))
text 1.57, 1.0) # x ≈ π/2, y = 1.0
text.setPos(
# إضافة النص إلى النافذة
win.addItem(text)
if __name__ == '__main__':
exec() pg.
text="..."
: النص المراد عرضه.color='r'
: لون النص (أحمر).anchor=(0.5, 1.2)
: يحدد نقطة التثبيت. (0.5, 1.2)
= منتصف النص من الأعلى، مع هامش أعلاه.setPos(x, y)
: لتحديد الموقع على الرسم.✅ مثالي للإشارة إلى القيم القصوى أو الحدث المهم.
ArrowItem
لإظهار اتجاه أو الإشارة إلى نقطة معينة.
# بعد إنشاء النافذة
= pg.ArrowItem(angle=-45, tipAngle=60, baseAngle=20, headLen=20, tailLen=40, brush='y', pen=None)
arrow 3, 0.5)
arrow.setPos(
win.addItem(arrow)
ArrowItem
الخيار | الوظيفة |
---|---|
angle |
زاوية السهم (بالدرجات) |
tipAngle , baseAngle |
زوايا الرأس والقاعدة |
headLen , tailLen |
طول الرأس والذيل |
brush |
لون التعبئة |
pen |
لون الحدودة |
✅
angle=-45
: يشير السهم إلى أسفل اليمين.
InfiniteLine
مفيد لتمثيل: - حدود قبول/رفض - المتوسطات - تواريخ أحداث
# خط رأسي عند x = 6.28 (نهاية الدورة)
= pg.InfiniteLine(pos=6.28, angle=90, pen=pg.mkPen('r', width=2, style=pg.QtCore.Qt.DashLine))
v_line
win.addItem(v_line)
# خط أفقي عند y = 0 (المحور الصفر)
= pg.InfiniteLine(pos=0, angle=0, pen=pg.mkPen('g', width=1))
h_line win.addItem(h_line)
✅
angle=90
: خط رأسي،angle=0
: خط أفقي.
LinearRegionItem
لإبراز فترة زمنية أو نطاق معين.
from pyqtgraph import LinearRegionItem
# إنشاء منطقة تظليل
= LinearRegionItem(values=(2, 4), orientation='vertical', brush=pg.mkBrush('r', 50)) # شفاف
region
win.addItem(region)
# يمكنك ربط الحدث بتحريك المنطقة
def on_region_changed():
print(f"المنطقة: {region.getRegion()}")
connect(on_region_changed) region.sigRegionChanged.
✅
brush=pg.mkBrush('r', 50)
: لون أحمر بشفافية 50.
الخطأ | السبب | الحل |
---|---|---|
النص لا يظهر | خارج نطاق العرض | استخدم autoRange() أو عدّل setPos() |
السهم لا يظهر | pen=None وbrush=None |
تأكد من تعيين brush |
الخط لا يظهر | pos خارج النطاق |
تأكد من قيمة pos |
addItem() لا يعمل |
لم تُنشئ العنصر أولًا | تأكد من pg.TextItem() إلخ |
x=5
بلون أخضر متقطع.x=1
وx=3
بلون أزرق شفاف.# فرض إشارة حية
= np.linspace(0, 10, 1000)
t = np.sin(t) + 0.1 * np.random.randn(1000)
signal
= pg.plot(title="تحليل الإشارة مع تعليقات")
win ='y')
win.plot(t, signal, pen
# حدث: انقطاع مؤقت عند t=5
= pg.InfiniteLine(pos=5, angle=90, pen=pg.mkPen('r', width=3))
v_line
win.addItem(v_line)
= pg.TextItem(text="انقطاع التيار", color='r', anchor=(0.5, 1.2))
text 5, 1.5)
text.setPos(
win.addItem(text)
# متوسط الإشارة
= np.mean(signal)
avg = pg.InfiniteLine(pos=avg, angle=0, pen=pg.mkPen('g', width=2, style=pg.QtCore.Qt.DashLine))
h_line
win.addItem(h_line)
= pg.TextItem(text=f"المتوسط: {avg:.2f}", color='g')
avg_text 0.5, avg + 0.2)
avg_text.setPos(
win.addItem(avg_text)
print("📌 تم إضافة التعليقات التوضيحية!")
✅ هذا النوع من التحليل يُستخدم في المراقبة الصناعية.
anchor
لضبط موضع النص بدقة.LinearRegionItem
للمناطق القابلة للتحريك.sigRegionChanged
لتحديث العناصر الأخرى.alpha
) لتجنب إخفاء البيانات.anchor
في TextItem
؟InfiniteLine
و LinearRegionItem
؟العنصر | الوظيفة |
---|---|
TextItem |
إضافة نص توضيحي |
ArrowItem |
إضافة سهم |
InfiniteLine |
خط رأسي أو أفقي لا نهائي |
LinearRegionItem |
تظليل منطقة قابلة للتحريك |
setPos() |
تحديد موقع العنصر |
sigRegionChanged |
ربط حدث تغيير المنطقة |
في واجهات المستخدم الرسومية (GUI)، لا تعمل البرامج خطوة بخطوة، بل تنتظر أحداثًا (Events) مثل: - النقر بالفأرة - الضغط على زر - التمرير - التحرك على عنصر
في PyQtGraph، تُسمى هذه الأحداث بـ الإشارات (Signals)، وهي طريقة لربط حدث ما بـ رد فعل (Function).
💡 مثل: “عند النقر على نقطة، اعرض قيمتها في نافذة منفصلة”.
فهم الإشارات هو المفتاح لبناء تطبيقات تفاعلية وذكية.
النمط العام هو:
connect(function) widget.signal.
widget
: العنصر (مثل: PlotItem
أو ViewBox
)signal
: الإشارة (مثل: sigClicked
أو sigRangeChanged
)function
: الدالة التي تُنفذ عند حدوث الحدثمثالي لعرض تفاصيل عند النقر على نقطة.
import pyqtgraph as pg
from PySide6.QtWidgets import QApplication, QMessageBox
import numpy as np
import sys
= QApplication(sys.argv)
app
# إنشاء نافذة
= pg.plot(title="النقر على النقاط")
win = np.random.normal(size=10)
x = np.random.normal(size=10)
y
# إنشاء نقاط
= pg.ScatterPlotItem(x=x, y=y, pen='w', brush='r', size=10, symbol='o')
scatter
win.addItem(scatter)
# دالة تُنفّذ عند النقر
def on_point_clicked(plot, points):
= points[0].index() # فهرس النقطة
index = f"النقطة {index}: X={x[index]:.2f}, Y={y[index]:.2f}"
msg print(msg)
# يمكن عرضها في نافذة منبثقة
# QMessageBox.information(None, "بيانات النقطة", msg)
# ربط الحدث
connect(on_point_clicked)
scatter.sigClicked.
if __name__ == '__main__':
exec() pg.
scatter.sigClicked.connect(...)
: يربط حدث النقر.points[0].index()
: يُرجع فهرس النقطة المُنقر عليها.QMessageBox
لعرض نافذة منبثقة.✅ مثالي لتحليل البيانات النقطية.
لتحديث عناصر واجهة أخرى عند تغيير العرض.
= win.getPlotItem()
plot_item = plot_item.getViewBox()
view_box
def on_zoom():
= view_box.viewRange()[0]
range_x = view_box.viewRange()[1]
range_y print(f"العرض الحالي: X({range_x[0]:.1f} → {range_x[1]:.1f}), Y({range_y[0]:.1f} → {range_y[1]:.1f})")
# ربط حدث تغيير مدى المحور X
connect(on_zoom) view_box.sigXRangeChanged.
✅ مفيد لتحديث تسميات أو عناصر تحكم أخرى.
لإضافة نقطة جديدة بالنقر.
def on_plot_clicked(event):
if event.button() == 2: # الزر الأيمن
# تحويل موقع الفأرة إلى إحداثيات الرسم
= event.pos()
pos = win.getViewBox()
view_box = view_box.mapSceneToView(pos)
point
# إضافة نقطة جديدة
= {'pos': (point.x(), point.y()), 'brush': 'b', 'size': 15}
new_point = [dict(pos=(x[i], y[i]), brush='r', size=10) for i in range(len(x))]
current_spots
current_spots.append(new_point)
scatter.clear()
scatter.addPoints(current_spots)
# ربط حدث النقر على الرسم
= win.getPlotItem()
plot_item connect(on_plot_clicked) plot_item.scene().sigMouseClicked.
✅
scene().sigMouseClicked
: للوصول إلى حدث الفأرة على النافذة.
مثالي لتحديد حدود ديناميكية.
= pg.InfiniteLine(pos=0, angle=90, movable=True, pen='y')
line
win.addItem(line)
def on_line_moved():
print(f"تم تحريك الخط إلى X = {line.getXPos()}")
connect(on_line_moved) line.sigDragged.
✅
movable=True
: يجعل الخط قابلاً للتحريك. ✅sigDragged
: يُطلق أثناء التحريك. ✅sigPositionChanged
: يُطلق بعد الانتهاء من التحريك.
الخطأ | السبب | الحل |
---|---|---|
AttributeError: 'PlotItem' object has no attribute 'sigClicked' |
حاولت ربط إشارة على PlotItem بدل ScatterPlotItem |
تأكد من نوع العنصر |
الدالة لا تُنفّذ | لم تُستخدم connect() |
تأكد من signal.connect(func) |
pos() غير معرف |
نسيت event.pos() |
استخدم event.pos() للحصول على موقع الفأرة |
لا يعمل النقر الأيمن | لم تتحقق من event.button() |
استخدم event.button() == 2 |
# فرض بيانات سعر سهم
= np.linspace(0, 10, 100)
t = 100 + np.cumsum(np.random.randn(100) * 0.5)
price
= pg.plot(title="تحليل التداول التفاعلي")
win = win.plot(t, price, pen='c')
curve
# عند النقر على الرسم، عرض السعر
def on_click(event):
if event.button() == 1: # الزر الأيسر
= event.pos()
pos = win.getViewBox()
view_box = view_box.mapSceneToView(pos)
point = point.x(), point.y()
x, y # تقريب إلى أقرب نقطة بيانات
= (np.abs(t - x)).argmin()
idx print(f"سعر السهم عند t={t[idx]:.1f}: {price[idx]:.2f} ريال")
connect(on_click)
win.scene().sigMouseClicked.
print("🖱️ انقر على أي نقطة في الرسم لعرض السعر!")
✅ هذا النوع من التفاعل يُستخدم في لوحات التداول.
sigClicked
مع ScatterPlotItem
فقط.scene().sigMouseClicked
للنقر على أي مكان في الرسم.mapSceneToView()
لتحويل موقع الفأرة إلى إحداثيات البيانات.sigDragged
للتحديث أثناء التحريك، وsigPositionChanged
للتحديث بعد الانتهاء.scatter
, line
) لتعديلها لاحقًا.sigClicked
و scene().sigMouseClicked
؟movable=True
؟العنصر | الإشارة | الوظيفة |
---|---|---|
ScatterPlotItem |
sigClicked |
عند النقر على نقطة |
ViewBox |
sigXRangeChanged |
عند التكبير/التحريك |
InfiniteLine |
sigDragged |
أثناء تحريك الخط |
PlotItem.scene() |
sigMouseClicked |
عند النقر على أي مكان |
الرسم اللحظي (Real-time plotting) هو القدرة على تحديث الرسم البياني بشكل مستمر مع وصول بيانات جديدة، دون تأخير ملحوظ.
يُستخدم في: - مراقبة مستشعرات IoT (درجة حرارة، رطوبة) - عرض أسعار الأسهم أو العملات الحية - تحليل الإشارات الكهربائية (مثل ECG) - تجارب علمية تتطلب مراقبة فورية
في هذا الدرس، ستتعلم كيفية بناء تطبيق PyQtGraph يُحدث الرسم بمعدل 60 إطارًا في الثانية، مع الحفاظ على الأداء.
لا يمكن استخدام time.sleep()
لأنه يُجمّد واجهة المستخدم.
الحل: استخدام مُؤقّت (Timer) من PyQt.
import pyqtgraph as pg
from PySide6.QtWidgets import QApplication
from PySide6.QtCore import QTimer
import numpy as np
import sys
= QApplication(sys.argv)
app
# إنشاء نافذة
= pg.plot(title="الرسم اللحظي - مثال بسيط")
win 'left', 'القيمة')
win.setLabel('bottom', 'الزمن')
win.setLabel(= win.plot(pen='y') # خط أصفر
curve
# بيانات افتراضية
= np.linspace(0, 50, 1000) # 1000 نقطة
x = 0
i
def update():
global i
if i >= len(x):
= 0 # إعادة التدوير
i = np.sin(x[:i+1] * 0.5) + 0.1 * np.random.randn(i+1) # بيانات ديناميكية
y +1], y) # تحديث البيانات
curve.setData(x[:i+= 1
i
# إنشاء مؤقت يُنفّذ update كل 50 ميلي ثانية (20 إطار/ثانية)
= QTimer()
timer connect(update)
timer.timeout.50)
timer.start(
if __name__ == '__main__':
exec() pg.
QTimer()
: يُنفّذ دالة بشكل دوري.timer.start(50)
: كل 50 ميلي ثانية.curve.setData()
: الطريقة الوحيدة لتحديث بيانات الرسم بسرعة.setData()
أسرع بكثير من plot()
.✅ لا تُنشئ خطوطًا جديدة في كل دورة!
عندما تصل البيانات بشكل مستمر، لا يمكن تخزينها إلى الأبد.
# حجم المخزن المؤقت
= 1000
buffer_size = np.linspace(0, 100, buffer_size)
x = np.zeros(buffer_size)
y = 0 # مؤشر الكتابة
ptr
= win.plot(x, y, pen='m')
curve
def update():
global ptr
= np.sin(0.1 * ptr) + 0.2 * np.random.randn() # قيمة جديدة
new_value = new_value
y[ptr] = (ptr + 1) % buffer_size # تدوير المؤشر
ptr
# تحديث العرض: عرض من ptr إلى النهاية، ثم من البداية إلى ptr
= np.concatenate([x[ptr:], x[:ptr]])
x_view = np.concatenate([y[ptr:], y[:ptr]])
y_view
curve.setData(x_view, y_view)
= QTimer()
timer connect(update)
timer.timeout.30) # ~33 إطار/ثانية timer.start(
✅ مثالي للبيانات التي تتدفق باستمرار.
لجعل الرسم أسرع: - تفعيل التفريغ (Downsampling) - تعطيل التحديثات غير الضرورية
# تفعيل التفريغ
='peak') # يُظهر الحد الأقصى والأدنى
win.setDownsampling(modeTrue) # فقط اعرض ما يظهر على الشاشة
win.setClipToView(
# زيادة حجم المخزن
= 10000 buffer_size
✅
setClipToView(True)
يُسرّع التحديث بشكل كبير.
= pg.ScatterPlotItem(pen=None, brush='r', size=8)
scatter
win.addItem(scatter)
= []
spots
def update_scatter():
global spots
= {'pos': (np.random.normal(), np.random.normal()), 'brush': 'b'}
new_spot
spots.append(new_spot)if len(spots) > 100: # احتفظ بأحدث 100 نقطة فقط
= spots[-100:]
spots
scatter.clear()
scatter.addPoints(spots)
connect(update_scatter) timer.timeout.
✅
clear()
وaddPoints()
للتحديث.
الخطأ | السبب | الحل |
---|---|---|
توقف الواجهة | استخدام time.sleep() |
استخدم QTimer |
بطء شديد | إنشاء خطوط جديدة في كل دورة | استخدم setData() |
البيانات لا تُحدّث | نسيت timer.start() |
تأكد من بدء المؤقت |
setData() لا يعمل |
تمرير بيانات خاطئة | تأكد من أن x و y نفس الطول |
setDownsampling()
و setClipToView(True)
لتحسين الأداء.# فرض سعر سهم يتحرك عشوائيًا
= 100.0
price = []
times = []
prices
= win.plot(pen='c')
curve
def update_stock():
global price, times, prices
+= np.random.randn() * 0.1 # تغير صغير
price = len(times)
current_time
times.append(current_time)
prices.append(price)
# احتفظ بأحدث 200 قيمة فقط
if len(times) > 200:
= times[-200:]
times = prices[-200:]
prices
# تحديث الرسم
curve.setData(times, prices)f"سعر السهم: {price:.2f} ريال")
win.setTitle(
= QTimer()
timer connect(update_stock)
timer.timeout.100) # كل 100 ميلي ثانية
timer.start(
print("📈 تحديث سعر السهم جارٍ الآن...")
✅ هذا هو الشكل الذي تُبنى عليه لوحات التداول.
setData()
بدل plot()
في الحلقات.QTimer
وليس time.sleep()
.setDownsampling(mode='peak')
للبيانات الكبيرة.setClipToView(True)
لتحسين الأداء.time.sleep()
في الرسم اللحظي؟setData()
؟setDownsampling()
و setClipToView()
؟العنصر | الوظيفة |
---|---|
QTimer |
تنفيذ دالة بشكل دوري |
setData() |
تحديث بيانات الرسم بسرعة |
setDownsampling() |
تفريغ البيانات لتحسين الأداء |
setClipToView(True) |
عرض ما يظهر فقط |
Circular Buffer |
تخزين بيانات دائرية |
GLViewWidget
بينما تُكفي الرسوم ثنائية الأبعاد (2D) لمعظم التحليلات، فإن بعض البيانات تتطلب بُعدًا ثالثًا لفهمها بشكل كامل.
تُستخدم الرسوم 3D في: - عرض السطوح ثلاثية الأبعاد (مثل: التضاريس، نتائج المحاكاة) - تحليل البيانات المكانية (X, Y, Z) - عرض الأنماط في البيانات ذات الأبعاد العالية - التطبيقات العلمية والهندسية (مثل: تدفق السوائل، مجالات الجاذبية)
في هذا الدرس، ستتعلم كيفية استخدام GLViewWidget
من PyQtGraph لعرض: - نقاط ثلاثية الأبعاد - سطوح (Surfaces) - شبكات (Meshes)
الرسوم 3D في PyQtGraph تعتمد على OpenGL، لذلك تأكد من: - تثبيت PyQtGraph
(الإصدار الأحدث) - تشغيل الكود على بيئة تدعم OpenGL
pip install pyqtgraph PySide6
✅ لا حاجة لتثبيت OpenGL يدويًا — PyQtGraph يتعامل معه داخليًا.
GLViewWidget
— نافذة العرض ثلاثية الأبعادهو العنصر الرئيسي لعرض الرسوم 3D.
import pyqtgraph.opengl as gl
from PySide6.QtWidgets import QApplication
import sys
= QApplication(sys.argv)
app
# إنشاء نافذة عرض 3D
= gl.GLViewWidget()
view
view.show()"نافذة 3D فارغة")
view.setWindowTitle(=50) # تحديد موضع الكاميرا
view.setCameraPosition(distance
if __name__ == '__main__':
exec() app.
✅
pyqtgraph.opengl as gl
: وحدة الرسوم 3D. ✅ يمكنك التفاعل بالفأرة: التدوير، التكبير، التحريك.
GLScatterPlotItem
import pyqtgraph.opengl as gl
import numpy as np
from PySide6.QtWidgets import QApplication
import sys
= QApplication(sys.argv)
app = gl.GLViewWidget()
view
view.show()"نقاط ثلاثية الأبعاد")
view.setWindowTitle(=40)
view.setCameraPosition(distance
# بيانات ثلاثية الأبعاد
= 1000
n = np.random.normal(size=n)
x = np.random.normal(size=n)
y = np.random.normal(size=n)
z = np.vstack((x, y, z)).T # مصفوفة بحجم (n, 3)
pos
# إنشاء نقاط
= gl.GLScatterPlotItem(pos=pos, color=(1, 0, 0, 1), size=0.5)
scatter
# إضافة النقاط إلى النافذة
view.addItem(scatter)
if __name__ == '__main__':
exec() app.
pos
: مصفوفة ثنائية الأبعاد بحجم (عدد_النقاط, 3)
color=(r, g, b, a)
: لون النقطة (أحمر، شفافية كاملة)size
: حجم النقطة✅ يمكنك تغيير لون كل نقطة باستخدام قائمة من الألوان.
GLSurfacePlotItem
مثالي لعرض الدوال الرياضية أو التضاريس.
import pyqtgraph.opengl as gl
import numpy as np
from PySide6.QtWidgets import QApplication
import sys
= QApplication(sys.argv)
app = gl.GLViewWidget()
view
view.show()"رسم سطح ثلاثي الأبعاد")
view.setWindowTitle(=40)
view.setCameraPosition(distance
# شبكة من النقاط (X, Y)
= np.linspace(-10, 10, 50)
x = np.linspace(-10, 10, 50)
y = np.meshgrid(x, y)
X, Y
# دالة Z = cos(r) حيث r = الجذر التربيعي لـ (X² + Y²)
= np.sqrt(X**2 + Y**2)
R = np.cos(R)
Z
# إنشاء السطح
= gl.GLSurfacePlotItem(x=x, y=y, z=Z, color=(0.5, 0.5, 1, 1), shader='shaded', drawEdges=False)
surface
# إضافة السطح
view.addItem(surface)
if __name__ == '__main__':
exec() app.
shader='shaded'
: إضافة تظليل واقعيdrawEdges=True
: عرض خطوط الشبكةcolor
: لون ثابت أو يمكن تغييره باستخدام setColors()
الخطأ | السبب | الحل |
---|---|---|
ModuleNotFoundError: No module named 'OpenGL' |
لم يتم تثبيت التبعيات | تأكد من pip install pyqtgraph |
لا تظهر النافذة 3D | بيئة غير مدعومة (مثل بعض الأجهزة الافتراضية) | جرب على جهاز به بطاقة رسومات |
النقاط لا تظهر | size صغير جدًا |
زد قيمة size |
السطح مشوّش | بيانات z غير منظمة |
تأكد من أن X , Y , Z نفس الشكل |
Z = X² + Y²
.# فرض بيانات تضاريس (مثل: ارتفاعات جبل)
= np.linspace(-5, 5, 100)
x = np.linspace(-5, 5, 100)
y = np.meshgrid(x, y)
X, Y # نموذج جبل مع عدة قمم
= 5 * np.exp(-((X-1)**2 + (Y-1)**2)) + \
Z 3 * np.exp(-((X+2)**2 + (Y+2)**2)) + \
2 * np.exp(-(X**2 + Y**2))
= gl.GLViewWidget()
view
view.show()"نموذج تضاريس رقمية")
view.setWindowTitle(=20)
view.setCameraPosition(distance
= gl.GLSurfacePlotItem(x=x, y=y, z=Z, shader='shaded', color=(0.2, 0.6, 0.2, 1)) # لون أخضر
surface
view.addItem(surface)
print("🏔️ نموذج التضاريس جاهز للعرض ثلاثي الأبعاد!")
✅ هذا النوع من العرض يُستخدم في نظم المعلومات الجغرافية (GIS).
GLViewWidget
فقط عند الحاجة إلى 3D.GLScatterPlotItem
للنقاط، وGLSurfacePlotItem
للسطوح.setCameraPosition()
لضبط العرض الابتدائي.GLViewWidget
؟pos
في GLScatterPlotItem
؟shader='shaded'
؟العنصر | الوظيفة |
---|---|
GLViewWidget |
نافذة العرض ثلاثية الأبعاد |
GLScatterPlotItem |
رسم نقاط 3D |
GLSurfacePlotItem |
رسم سطوح 3D |
pos |
مصفوفة (n, 3) للنقاط |
x, y, z |
شبكات للسطوح |
setCameraPosition() |
ضبط الكاميرا |
shader='shaded' |
تظليل واقعي |
الخريطة اللونية (Colormap) هي وسيلة لتمثيل القيم العددية باستخدام التدرجات اللونية.
بدلاً من عرض الأرقام فقط، يمكنك: - تمثيل الحرارة بألوان من الأزرق (بارد) إلى الأحمر (ساخن) - عرض كثافة البيانات من الشفاف إلى المعتم - تمييز القيم العالية والمنخفضة في الصور أو المصفوفات
تُستخدم الخرائط اللونية في: - ImageView (عرض الصور الحرارية، الأشعة) - GLSurfacePlotItem (رسم السطوح 3D) - الرسوم البيانية المكانية (مثل الخرائط الجغرافية)
يأتي PyQtGraph مع مجموعة من الخرائط اللونية الجاهزة.
import pyqtgraph as pg
from PySide6.QtWidgets import QApplication
import numpy as np
import sys
= QApplication(sys.argv)
app
# إنشاء بيانات صورة (مثل: توزيع حرارة)
= np.random.normal(size=(100, 100)) * 50 + 100
image_data
# إنشاء ImageView
= pg.ImageView()
view
view.setImage(image_data)
# تطبيق خريطة لونية جاهزة
= pg.colormap.get('viridis') # أو 'plasma', 'hot', 'cool', 'gray', 'jet'
cmap
view.setColorMap(cmap)
"خريطة لونية: viridis")
view.setWindowTitle(
view.show()
if __name__ == '__main__':
exec() pg.
الخريطة | الاستخدام |
---|---|
'viridis' |
افتراضية، واضحة، مناسبة للطباعة |
'plasma' |
تباين عالي، من الأرجواني إلى الأصفر |
'hot' |
من الأسود إلى الأحمر إلى الأصفر (مثل النار) |
'cool' |
من الأزرق إلى الوردي |
'gray' |
تدرج رمادي (مثالي للصور الطبية) |
'jet' |
تدرج كامل (مثير بصريًا، لكنه غير دقيق) |
✅
pg.colormap.get('name')
: للحصول على خريطة لونية.
يمكنك تعريف تدرج لوني خاص بك.
# تحديد الألوان عند نقاط معينة (من 0.0 إلى 1.0)
= [
colors 0, 0, 255), # أزرق (قيمة منخفضة)
(0, 255, 255), # سماوي
(0, 255, 0), # أخضر
(255, 255, 0), # أصفر
(255, 0, 0) # أحمر (قيمة عالية)
(
]
# إنشاء الخريطة
= pg.ColorMap(pos=np.linspace(0.0, 1.0, len(colors)), color=colors)
custom_cmap
# تطبيقها على ImageView
view.setColorMap(custom_cmap)
pos
: مواقع النسب (0.0 = القيمة الدنيا، 1.0 = القيمة العليا)color
: قائمة بألوان RGB (من 0 إلى 255)import pyqtgraph.opengl as gl
import numpy as np
from PySide6.QtWidgets import QApplication
import sys
= QApplication(sys.argv)
app = gl.GLViewWidget()
view
view.show()"سطح 3D مع خريطة لونية")
view.setWindowTitle(=40)
view.setCameraPosition(distance
# إنشاء شبكة
= np.linspace(-10, 10, 50)
x = np.linspace(-10, 10, 50)
y = np.meshgrid(x, y)
X, Y = np.cos(np.sqrt(X**2 + Y**2))
Z
# إنشاء خريطة لونية
= pg.colormap.get('plasma')
cmap
# تحويل الخريطة إلى ألوان لكل نقطة
= (Z - Z.min()) / (Z.max() - Z.min()) # من 0 إلى 1
normalized_z = cmap.getLookupTable(nPts=256)
colors = np.array([colors[int(i*255)] for i in normalized_z.flatten()])
surface_colors = surface_colors.reshape((*Z.shape, 4)) # (H, W, 4)
surface_colors
# إنشاء السطح
= gl.GLSurfacePlotItem(x=x, y=y, z=Z, colors=surface_colors, shader='shaded')
surface
view.addItem(surface)
if __name__ == '__main__':
exec() app.
✅ يتم تحويل القيم إلى نطاق [0,1] ثم تعيين لون لكل قيمة.
لإضافة شريط توضيحي يُظهر العلاقة بين اللون والقيمة.
# بعد إنشاء ImageView
= pg.ImageView()
view
view.setImage(image_data)'hot'))
view.setColorMap(pg.colormap.get(
# إضافة شريط الألوان
= pg.ColorBarItem(values=(image_data.min(), image_data.max()), colorMap=pg.colormap.get('hot'))
color_bar 2, 1) # إضافته في الصف 2، العمود 1
view.layout.addItem(color_bar,
view.show()
✅
ColorBarItem
يُظهر مقياس الألوان.
الخطأ | السبب | الحل |
---|---|---|
AttributeError: 'ImageView' has no attribute 'setColorMap' |
استخدمت setColorMap بدل setColorMap |
التهجئة الصحيحة: setColorMap |
الألوان لا تظهر | لم تُستخدم colors بشكل صحيح في 3D |
تأكد من شكل colors (يجب أن يكون (H, W, 4) ) |
الشريط لا يظهر | لم تُضف ColorBarItem إلى التخطيط |
استخدم layout.addItem() |
getLookupTable() لا يعمل |
نسيت تحديد nPts |
استخدم cmap.getLookupTable(nPts=256) |
cool
على صورة مصفوفة.ImageView
.Z
.# فرض بيانات حرارة (مثل: من مستشعرات في الجدران)
= np.random.rand(50, 50) * 30 + 20 # 20-50 درجة مئوية
heat_map
= pg.ImageView()
view
view.setImage(heat_map)'hot'))
view.setColorMap(pg.colormap.get(
# شريط توضيحي
= pg.ColorBarItem(values=(20, 50), colorMap=pg.colormap.get('hot'), label='درجة الحرارة (°C)')
color_bar 2, 1)
view.layout.addItem(color_bar,
"تحليل توزيع الحرارة")
view.setWindowTitle(
view.show()
print("🌡️ تحليل الحرارة جاهز للعرض!")
✅ هذا النوع من التحليل يُستخدم في أنظمة مراقبة المباني.
'viridis'
أو 'plasma'
للعروض العامة.'gray'
للصور الطبية.'jet'
في التقارير الرسمية (غير دقيق بصريًا).ColorBarItem
لجعل الرسم مفهومًا.levels
في ImageView
لتحسين التباين.ImageView
؟pg.colormap.get()
؟العنصر | الوظيفة |
---|---|
pg.colormap.get('name') |
الحصول على خريطة لونية |
setColorMap() |
تطبيق الخريطة على ImageView |
ColorMap(pos, color) |
إنشاء خريطة مخصصة |
ColorBarItem |
شريط توضيحي للون |
getLookupTable() |
تحويل الخريطة إلى ألوان |
في التطبيقات الواقعية، نادرًا ما يكفي رسم واحد لفهم الصورة الكاملة.
غالبًا ما نحتاج إلى عرض: - مخططات مقارنة (مثل: المبيعات مقابل التكاليف) - بيانات متعددة الأبعاد (مثل: درجة الحرارة، الرطوبة، الضغط في نفس الوقت) - تحليل متعدد الجوانب (مثل: السعر، الحجم، مؤشرات فنية)
في هذا الدرس، ستتعلم كيفية استخدام GraphicsLayoutWidget
لتنظيم عدة رسومات في نافذة واحدة، مع التحكم الكامل في التخطيط والتنسيق.
GraphicsLayoutWidget
— حاوية الرسومات المتعددةكما تعلمت سابقًا، GraphicsLayoutWidget
هو العنصر المثالي لعرض أكثر من رسم.
import pyqtgraph as pg
from PySide6.QtWidgets import QApplication
import numpy as np
import sys
= QApplication(sys.argv)
app
# إنشاء حاوية متعددة الرسومات
= pg.GraphicsLayoutWidget(show=True, title="رسمان جنبًا إلى جنب")
win 1000, 500)
win.resize(
# الرسم الأول: جيب التمام
= win.addPlot(row=0, col=0, title="cos(x)")
p1 = np.linspace(0, 4*np.pi, 100)
x = np.cos(x)
y1 ='b')
p1.plot(x, y1, pen
# الرسم الثاني: الجيب
= win.addPlot(row=0, col=1, title="sin(x)")
p2 = np.sin(x)
y2 ='r')
p2.plot(x, y2, pen
if __name__ == '__main__':
exec() pg.
addPlot(row, col)
: يُضيف رسمًا في موقع معين.row=0, col=0
: الصف 0، العمود 0 (أعلى اليسار)row=0, col=1
: الصف 0، العمود 1 (أعلى اليمين)✅ مثالي للعرض المقارن.
= pg.GraphicsLayoutWidget(show=True, title="مخططات 2×2")
win 1000, 800)
win.resize(
# إنشاء الرسومات
= win.addPlot(row=0, col=0, title="cos(x)")
p1 ='b')
p1.plot(x, np.cos(x), pen
= win.addPlot(row=0, col=1, title="sin(x)")
p2 ='r')
p2.plot(x, np.sin(x), pen
= win.addPlot(row=1, col=0, title="cos²(x)")
p3 **2, pen='g')
p3.plot(x, np.cos(x)
= win.addPlot(row=1, col=1, title="sin²(x)")
p4 **2, pen='m') p4.plot(x, np.sin(x)
✅ يمكنك إنشاء أي تخطيط شبكي.
مفيد عندما تريد التكبير والتحريك على أكثر من رسم معًا.
# بعد إنشاء p1 و p2
# ربط محور X للرسم الثاني بالرسم الأول p2.setXLink(p1)
✅ الآن، عند التكبير على
p1
، سيتكبّرp2
بنفس النطاق.
p2.setYLink(p1)
✅ مفيد في الرسومات التي تستخدم نفس المقياس.
لتحسين المظهر البصري.
'left') # إخفاء المحور الأيسر
p2.hideAxis('right') # (اختياري) إظهار محور على اليمين p2.showAxis(
✅ يجعل التخطيط أكثر انسيابية.
لإضافة عناوين أو فواصل.
from pyqtgraph import LabelItem
= LabelItem("تحليل دوال مثلثية", color='w', size=20)
label =0, col=0, colspan=2) # تمتد على عمودين win.addItem(label, row
✅
colspan=2
: يُغطي عمودين.
الخطأ | السبب | الحل |
---|---|---|
لا تظهر الرسومات | نسيت addPlot() |
تأكد من استخدام addPlot(row, col) |
التكبير لا يعمل | لم يتم تفعيل ViewBox |
تأكد من أن mouseEnabled=True |
setXLink() لا يعمل |
لم يتم ربط الرسومات بشكل صحيح | تأكد من ترتيب الإنشاء |
التخطيط مشوّش | لم تُضبط الحجم | استخدم win.resize() |
sin(x)
, cos(x)
, وtan(x)
.# فرض 4 إشارات (مثل: مستشعرات في مصنع)
= np.linspace(0, 10, 1000)
t = {
signals 'درجة الحرارة': 25 + 5 * np.sin(t) + np.random.normal(0, 0.5, 1000),
'الرطوبة': 60 + 10 * np.cos(t) + np.random.normal(0, 1, 1000),
'الضغط': 1013 + np.random.normal(0, 5, 1000),
'الاهتزاز': 0.1 * np.random.randn(1000)
}
= pg.GraphicsLayoutWidget(show=True, title="لوحة مراقبة المصنع", size=(1200, 800))
win
= {}
plots for i, (name, data) in enumerate(signals.items()):
= i // 2
row = i % 2
col = win.addPlot(row=row, col=col, title=name)
plots[name] ='y')
plots[name].plot(t, data, pen=True, alpha=0.3)
plots[name].showGrid(yif col > 0:
'left')
plots[name].hideAxis(
# مزامنة محور الزمن
for name, p in plots.items():
if name != 'درجة الحرارة':
'درجة الحرارة'])
p.setXLink(plots[
print("📊 لوحة المراقبة جاهزة للتشغيل اللحظي!")
✅ هذا النوع من الواجهات يُستخدم في أنظمة SCADA.
GraphicsLayoutWidget
دائمًا للرسومات المتعددة.setXLink()
لربط الرسومات الزمنية.hideAxis()
لتحسين التصميم.LabelItem
لإضافة عناوين.setXLink()
؟GraphicsLayoutWidget
و PlotWidget
؟العنصر | الوظيفة |
---|---|
GraphicsLayoutWidget |
حاوية للرسومات المتعددة |
addPlot(row, col) |
إضافة رسم في موقع معين |
setXLink(p) |
مزامنة محور X |
setYLink(p) |
مزامنة محور Y |
hideAxis('left') |
إخفاء محور |
LabelItem |
إضافة نص |
colspan |
تمديد العنصر على أعمدة |
حتى الآن، كنا نستخدم PlotWidget
و ImageView
بشكل منفصل.
لكن في التطبيقات الحقيقية، نادرًا ما تكون الرسومات هي الشيء الوحيد في الواجهة.
غالبًا ما نحتاج إلى: - إضافة أزرار تحكم (تشغيل، إيقاف، تحديث) - إضافة مربعات نصية (إدخال معلمات) - إضافة قوائم منسدلة (اختيار نوع البيانات) - دمج الرسم مع جداول أو نصوص توضيحية
لهذا، يجب أن نتعلم كيفية دمج عناصر PyQtGraph داخل واجهات PyQt/PySide الأكبر، مثل QMainWindow
أو QDialog
.
PlotWidget
داخل QMainWindow
import pyqtgraph as pg
from PySide6.QtWidgets import (
QApplication, QMainWindow, QWidget, QVBoxLayout, QPushButton
)from PySide6.QtCore import QTimer
import numpy as np
import sys
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("تطبيق مراقبة مع أزرار")
self.resize(800, 600)
# إنشاء عنصر مركزي
= QWidget()
central_widget self.setCentralWidget(central_widget)
# إنشاء تخطيط عمودي
= QVBoxLayout(central_widget)
layout
# إنشاء الرسم
self.plot_widget = pg.PlotWidget(title="بيانات حية")
self.plot_widget.setLabel('left', 'القيمة')
self.plot_widget.setLabel('bottom', 'الزمن')
self.curve = self.plot_widget.plot(pen='y')
# إضافة الرسم إلى التخطيط
self.plot_widget)
layout.addWidget(
# إضافة زر
self.btn_toggle = QPushButton("إيقاف/تشغيل التحديث")
self.btn_toggle.setCheckable(True)
self.btn_toggle.setChecked(True)
self.btn_toggle.toggled.connect(self.toggle_timer)
self.btn_toggle)
layout.addWidget(
# بيانات افتراضية
self.x = np.linspace(0, 50, 1000)
self.i = 0
# مؤقت التحديث
self.timer = QTimer()
self.timer.timeout.connect(self.update_plot)
self.timer.start(50)
def update_plot(self):
if not self.btn_toggle.isChecked():
return
= np.sin(self.x[:self.i+1] * 0.5) + 0.1 * np.random.randn(self.i+1)
y self.curve.setData(self.x[:self.i+1], y)
self.i = (self.i + 1) % len(self.x)
def toggle_timer(self, checked):
if checked:
self.timer.start(50)
else:
self.timer.stop()
= QApplication(sys.argv)
app = MainWindow()
window
window.show()exec() app.
QMainWindow
: النافذة الرئيسية.QWidget
+ QVBoxLayout
: لتنظيم العناصر.setCentralWidget()
: لتحديد العنصر المركزي.QPushButton
مع setCheckable(True)
: زر تبديل.✅ هذا هو الشكل الأساسي لأي تطبيق تفاعلي.
GraphicsLayoutWidget
في التخطيطات المعقدةfrom PySide6.QtWidgets import QGridLayout
# داخل __init__ بعد إنشاء central_widget
= QGridLayout(central_widget)
layout
# إنشاء رسمين
self.plot1 = pg.PlotWidget(title="الرسم 1")
self.plot2 = pg.PlotWidget(title="الرسم 2")
# إضافة الرسومات إلى التخطيط
self.plot1, 0, 0) # الصف 0، العمود 0
layout.addWidget(self.plot2, 0, 1) # الصف 0، العمود 1 layout.addWidget(
✅ مثالي للواجهات التي تدمج نصوص، أزرار، ورسومات.
ImageView
مع عناصر تحكمfrom PySide6.QtWidgets import QFileDialog
# إضافة زر لتحميل صورة
self.btn_load = QPushButton("تحميل صورة")
self.btn_load.clicked.connect(self.load_image)
self.btn_load)
layout.addWidget(
# إنشاء ImageView
self.image_view = pg.ImageView()
self.image_view)
layout.addWidget(
def load_image(self):
= QFileDialog.getOpenFileName(
file_path, _ self, "اختر صورة", "", "Images (*.png *.xpm *.jpg *.bmp)"
)if file_path:
# في الواقع، يجب استخدام مكتبة مثل `imageio` أو `Pillow`
# لكن لهذا المثال، نُنشئ بيانات وهمية
import numpy as np
= np.random.rand(200, 200) * 255
fake_image self.image_view.setImage(fake_image)
✅ في التطبيقات الحقيقية، استخدم
imageio.imread()
أوPillow
.
الخطأ | السبب | الحل |
---|---|---|
لا تظهر الرسومات | لم تُضف إلى التخطيط | تأكد من layout.addWidget(plot) |
الزر لا يعمل | لم تُربط الإشارة | تأكد من clicked.connect() |
PlotWidget لا يأخذ المساحة |
لم يتم تعيين التخطيط بشكل صحيح | تأكد من setCentralWidget() |
QFileDialog لا يظهر |
لم يتم استدعاء exec() |
استخدم getOpenFileName() بشكل صحيح |
QMainWindow
به PlotWidget
وزر “إعادة تعيين”.ImageView
مع زر “تحديث البيانات”.class MonitoringApp(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("لوحة مراقبة متكاملة")
self.resize(1200, 800)
= QWidget()
central self.setCentralWidget(central)
= QVBoxLayout(central)
layout
# تخطيط رئيسي (على شكل شبكة)
= QGridLayout()
main_layout
layout.addLayout(main_layout)
# رسومات
self.temp_plot = pg.PlotWidget(title="درجة الحرارة")
self.humi_plot = pg.PlotWidget(title="الرطوبة")
self.pres_plot = pg.PlotWidget(title="الضغط")
self.temp_plot, 0, 0)
main_layout.addWidget(self.humi_plot, 0, 1)
main_layout.addWidget(self.pres_plot, 1, 0, 1, 2) # يمتد على عمودين
main_layout.addWidget(
# عناصر التحكم
= QHBoxLayout()
control_layout self.btn_start = QPushButton("بدء")
self.btn_stop = QPushButton("إيقاف")
self.btn_reset = QPushButton("إعادة تعيين")
self.btn_start)
control_layout.addWidget(self.btn_stop)
control_layout.addWidget(self.btn_reset)
control_layout.addWidget(
layout.addLayout(control_layout)
# المؤقت
self.timer = QTimer()
self.setup_plots()
self.btn_start.clicked.connect(self.start_monitoring)
self.btn_stop.clicked.connect(self.stop_monitoring)
self.btn_reset.clicked.connect(self.reset_data)
def setup_plots(self):
# إعداد الرسومات
pass
def start_monitoring(self):
self.timer.start(100)
def stop_monitoring(self):
self.timer.stop()
def reset_data(self):
# إعادة تعيين البيانات
pass
# تشغيل التطبيق
= QApplication(sys.argv)
app = MonitoringApp()
window
window.show()exec() app.
✅ هذا هو الشكل الذي تُبنى عليه التطبيقات الصناعية.
QMainWindow
كنقطة بداية لأي تطبيق كبير.QVBoxLayout
أو QGridLayout
لتنظيم العناصر.self.plot_widget
) للوصول إليها لاحقًا.setCheckable(True)
للأزرار التي تُغيّر الحالة.PlotWidget
إلى QMainWindow
؟QVBoxLayout
و QGridLayout
؟العنصر | الوظيفة |
---|---|
QMainWindow |
النافذة الرئيسية |
setCentralWidget() |
تعيين العنصر المركزي |
QVBoxLayout |
تخطيط عمودي |
QGridLayout |
تخطيط شبكي |
QPushButton |
زر تحكم |
clicked.connect() |
ربط حدث الزر |
setCheckable(True) |
جعل الزر قابلاً للتبديل |
بعد أن تحلّل البيانات، وتُنظّفها، وتُنتج رؤى قيّمة، يأتي الوقت لـ: - مشاركة النتائج مع الفريق أو الإدارة - استخدام البيانات في تطبيقات أخرى (مثل: Excel، تقارير Power BI) - حفظ النتائج الوسيطة لاستخدامها لاحقًا - أرشفة التحليل للرجوع إليه
لهذا، نحتاج إلى أدوات قوية وآمنة لـ حفظ البيانات وتصدير الرسومات.
في هذا الدرس، ستتعلم كيفية: - حفظ DataFrame إلى ملفات - أخذ لقطات (Screenshot) من رسومات PyQtGraph - تصدير النتائج بشكل احترافي
# أفضل إعدادات لملفات CSV
'results.csv',
df.to_csv(=False,
index='utf-8-sig', # ضروري لعرض العربية في إكسل
encoding=',') sep
✅
utf-8-sig
: يحل مشكلة عرض النصوص العربية في Excel.
with pd.ExcelWriter('report.xlsx', engine='openpyxl') as writer:
='المبيعات', index=False)
df_sales.to_excel(writer, sheet_name='الملخص', index=False)
df_summary.to_excel(writer, sheet_name='الاتجاهات', index=False) df_trends.to_excel(writer, sheet_name
✅ مثالي للتقارير الإدارية.
'api_data.json',
df.to_json(=False, # يسمح بالحروف غير الإنجليزية
force_ascii='records', # كل صف ككائن
orient=4) # تنسيق جميل indent
✅ جاهز للإرسال عبر API.
PlotWidget
import pyqtgraph as pg
from PySide6.QtWidgets import QApplication
import numpy as np
import sys
= QApplication(sys.argv)
app
# إنشاء رسم
= pg.plot(title="بيانات المبيعات")
win = np.linspace(0, 10, 100)
x = np.sin(x)
y ='b')
win.plot(x, y, pen
# حفظ لقطة
"plot_screenshot.png")
win.grab().save(
print("✅ تم حفظ اللقطة كـ plot_screenshot.png")
if __name__ == '__main__':
exec() pg.
win.grab()
: يلتقط صورة من النافذة..save("filename.png")
: يحفظ الصورة.✅ التنسيقات المدعومة: PNG, JPG, BMP.
ImageView
= pg.ImageView()
view 100, 100))
view.setImage(np.random.rand("تحليل حرارة")
view.setWindowTitle(
# بعد عرض النافذة
view.show()"thermal_analysis.png") view.grab().save(
✅ مثالي لعرض النتائج في التقارير.
from PySide6.QtWidgets import QPushButton, QFileDialog
# داخل واجهة المستخدم
self.btn_save_plot = QPushButton("حفظ الرسم كصورة")
self.btn_save_plot.clicked.connect(self.save_plot_screenshot)
def save_plot_screenshot(self):
# فتح نافذة اختيار الملف
= QFileDialog.getSaveFileName(
file_path, _ self, "حفظ الصورة", "", "Images (*.png *.jpg *.bmp)"
)if file_path:
self.plot_widget.grab().save(file_path)
print(f"تم حفظ الصورة في: {file_path}")
✅ يجعل التطبيق تفاعليًا.
الخطأ | السبب | الحل |
---|---|---|
النص العربي مشوّش في CSV | ترميز خاطئ | استخدم encoding='utf-8-sig' |
grab() لا يعمل |
النافذة لم تُعرض بعد | تأكد من show() قبل grab() |
QFileDialog لا يظهر |
لم يتم استدعاء exec() |
استخدم getSaveFileName() بشكل صحيح |
ملف Excel فارغ | لم تُستخدم with أو writer |
تأكد من إغلاق الـ Writer |
# افترض أن لديك نتائج تحليل
= df.groupby('القسم')['الراتب'].agg(['mean', 'sum', 'count'])
summary_report
# 1. حفظ كـ CSV للتحليل الآلي
'monthly_report.csv', encoding='utf-8-sig')
summary_report.to_csv(
# 2. حفظ كـ Excel لتوزيعه على الإدارة
with pd.ExcelWriter('monthly_report_manager.xlsx') as writer:
='الملخص')
summary_report.to_excel(writer, sheet_name='البيانات_الكاملة', index=False)
df.to_excel(writer, sheet_name
# 3. أخذ لقطة من الرسم البياني
'salary_chart.png')
plot_win.grab().save(
print("✅ التقرير والرسم تم تصديرهما بنجاح.")
✅ هذا هو الشكل النهائي لأي مشروع تحليل بيانات.
index=False
ما لم تكن بحاجة للفهرس.utf-8-sig
لضمان ظهور العربية بشكل صحيح.QFileDialog
للسماح للمستخدم باختيار الموقع.# تم الحفظ في ./output/
utf-8
و utf-8-sig
؟PlotWidget
؟orient='records'
في to_json()
؟الوظيفة | الكود |
---|---|
حفظ CSV | to_csv('file.csv', index=False, encoding='utf-8-sig') |
حفظ Excel متعدد الأوراق | with pd.ExcelWriter(...) as writer: |
حفظ JSON | to_json(..., force_ascii=False, orient='records') |
أخذ لقطة | widget.grab().save('image.png') |
فتح نافذة حفظ | QFileDialog.getSaveFileName() |
بالطبع! إليك:
✅ عدد الكلمات: ~1600
لقد تعلمت حتى الآن عشرات الأدوات والتقنيات في PyQtGraph.
الآن حان الوقت لدمج كل ما تعلمته في مشروع تطبيقي واقعي.
هذا الدرس يُشبه “الامتحان النهائي”، حيث ستُبنى تطبيقًا متكاملًا يُظهر: - عرض بيانات حية - رسومات متعددة - تفاعل مع المستخدم - واجهة مستخدم مخصصة
🎯 الهدف النهائي: بناء تطبيق مراقبة مستشعرات في الزمن الحقيقي.
تخيل أنك تعمل على نظام يراقب بيئة معمل أو مصنع.
هناك 3 مستشعرات: 1. درجة الحرارة (°C) 2. الرطوبة (%) 3. الاهتزاز (وحدة قياس وهمية)
البيانات تصل بشكل لحظي، ويجب عرضها على شاشة مراقبة تحتوي على: - 3 رسومات منفصلة - أزرار للتحكم (تشغيل/إيقاف، إعادة تعيين) - عرض قيم حالية - لون خلفية يُشير إلى حالة النظام (أخضر = طبيعي، أحمر = إنذار)
monitoring_app.py
│
├── MainWindow (QMainWindow)
│ ├── Central Widget
│ │ ├── Main Layout (QVBoxLayout)
│ │ │ ├── Control Layout (QHBoxLayout)
│ │ │ │ ├── QPushButton: Start/Stop
│ │ │ │ ├── QPushButton: Reset
│ │ │ │ └── QLabel: القيم الحالية
│ │ │ ├── Temperature Plot (PlotWidget)
│ │ │ ├── Humidity Plot (PlotWidget)
│ │ │ └── Vibration Plot (PlotWidget)
│ │ └── QTimer: لتحديث البيانات
│ └── Data Buffer: لتخزين البيانات
import pyqtgraph as pg
from PySide6.QtWidgets import (
QApplication, QMainWindow, QWidget, QVBoxLayout,
QHBoxLayout, QPushButton, QLabel
)from PySide6.QtCore import QTimer
from PySide6.QtGui import QFont
import numpy as np
import sys
class SensorMonitor(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("مراقبة المستشعرات اللحظية")
self.resize(1200, 800)
# العنصر المركزي
= QWidget()
central_widget self.setCentralWidget(central_widget)
# التخطيط الرئيسي
= QVBoxLayout(central_widget)
main_layout
# تخطيط عناصر التحكم
= QHBoxLayout()
control_layout self.btn_start = QPushButton("بدء المراقبة")
self.btn_start.setCheckable(True)
self.btn_start.setChecked(True)
self.btn_start.toggled.connect(self.toggle_monitoring)
self.btn_reset = QPushButton("إعادة تعيين")
self.btn_reset.clicked.connect(self.reset_data)
# عرض القيم الحالية
self.current_values = QLabel("القيم الحالية: --, --, --")
self.current_values.setFont(QFont("Arial", 12))
self.current_values.setStyleSheet("background-color: #f0f0f0; padding: 10px; border-radius: 5px;")
self.btn_start)
control_layout.addWidget(self.btn_reset)
control_layout.addWidget(self.current_values)
control_layout.addWidget(
main_layout.addLayout(control_layout)
# إعداد المخزن المؤقت
self.buffer_size = 200
self.time = np.linspace(0, 20, self.buffer_size) # 20 ثانية
self.temp_data = np.zeros(self.buffer_size)
self.humi_data = np.zeros(self.buffer_size)
self.vibe_data = np.zeros(self.buffer_size)
self.ptr = 0 # مؤشر الكتابة
# إعداد الرسومات
self.setup_plots(main_layout)
# المؤقت
self.timer = QTimer()
self.timer.timeout.connect(self.update_plots)
self.timer.start(100) # كل 100 ميلي ثانية
def setup_plots(self, layout):
"""إنشاء وإعداد الرسومات الثلاثة"""
# درجة الحرارة
self.temp_plot = pg.PlotWidget(title="درجة الحرارة (°C)")
self.temp_plot.setLabel('left', 'درجة الحرارة')
self.temp_plot.setLabel('bottom', 'الزمن (ثانية)')
self.temp_plot.showGrid(y=True, alpha=0.3)
self.temp_curve = self.temp_plot.plot(pen='r', name='درجة الحرارة')
self.temp_plot)
layout.addWidget(
# الرطوبة
self.humi_plot = pg.PlotWidget(title="الرطوبة (%)")
self.humi_plot.setLabel('left', 'الرطوبة')
self.humi_plot.setLabel('bottom', 'الزمن (ثانية)')
self.humi_plot.showGrid(y=True, alpha=0.3)
self.humi_curve = self.humi_plot.plot(pen='b', name='الرطوبة')
self.humi_plot)
layout.addWidget(
# الاهتزاز
self.vibe_plot = pg.PlotWidget(title="الاهتزاز")
self.vibe_plot.setLabel('left', 'الاهتزاز')
self.vibe_plot.setLabel('bottom', 'الزمن (ثانية)')
self.vibe_plot.showGrid(y=True, alpha=0.3)
self.vibe_curve = self.vibe_plot.plot(pen='m', name='الاهتزاز')
self.vibe_plot)
layout.addWidget(
def generate_sensor_data(self):
"""توليد بيانات وهمية للمستشعرات"""
# درجة الحرارة: 25 ± 5 مع تذبذب
= 25 + 5 * np.sin(0.1 * self.ptr) + np.random.normal(0, 0.5)
temp # الرطوبة: 60 ± 10
= 60 + 10 * np.cos(0.08 * self.ptr) + np.random.normal(0, 1)
humi # الاهتزاز: عادة منخفض، لكن به قفزات عرضية
= 0.5 + np.random.normal(0, 0.1)
vibe if np.random.rand() < 0.05: # 5% فرصة لاهتزاز عالي
+= np.random.rand() * 5
vibe
return temp, humi, vibe
def update_plots(self):
"""تحديث جميع الرسومات"""
if not self.btn_start.isChecked():
return
= self.generate_sensor_data()
temp, humi, vibe
# تحديث المخزن
self.temp_data[self.ptr] = temp
self.humi_data[self.ptr] = humi
self.vibe_data[self.ptr] = vibe
# تحديث المنحنيات
self.update_curve(self.temp_curve, self.temp_data)
self.update_curve(self.humi_curve, self.humi_data)
self.update_curve(self.vibe_curve, self.vibe_data)
# تحديث العرض
self.current_values.setText(
f"القيم الحالية: {temp:.1f}°C, {humi:.1f}%, {vibe:.2f}"
)
# تغيير لون الخلفية إذا كانت هناك إنذار
if vibe > 3.0:
self.current_values.setStyleSheet("background-color: #ffcccc; padding: 10px; border-radius: 5px;")
else:
self.current_values.setStyleSheet("background-color: #ccffcc; padding: 10px; border-radius: 5px;")
# تقدم المؤشر
self.ptr = (self.ptr + 1) % self.buffer_size
def update_curve(self, curve, data):
"""تحديث منحنى معين"""
= self.ptr
ptr # عرض البيانات من المؤشر إلى النهاية، ثم من البداية إلى المؤشر (للسلاسة)
= np.concatenate([data[ptr:], data[:ptr]])
y_view self.time, y_view)
curve.setData(
def reset_data(self):
"""إعادة تعيين جميع البيانات"""
self.temp_data[:] = 0
self.humi_data[:] = 0
self.vibe_data[:] = 0
self.ptr = 0
self.current_values.setText("القيم الحالية: --, --, --")
self.current_values.setStyleSheet("background-color: #f0f0f0; padding: 10px; border-radius: 5px;")
def toggle_monitoring(self, checked):
if checked:
self.btn_start.setText("إيقاف المراقبة")
else:
self.btn_start.setText("بدء المراقبة")
if __name__ == '__main__':
= QApplication(sys.argv)
app = SensorMonitor()
window
window.show()exec() app.
QMainWindow
: النافذة الرئيسية.QVBoxLayout
: تخطيط رئيسي عمودي.QHBoxLayout
: تخطيط أفقي للعناصر العليا.setCheckable(True)
لتبديل الحالة.QLabel
: يعرض القيم الحالية بلون خلفية ديناميكي.buffer_size
: حجم المخزن (200 نقطة).time
: مصفوفة زمن ثابتة.ptr
: مؤشر يدور حول المخزن (Circular Buffer).PlotWidget
لكل مستشعر.showGrid
) لتحسين القراءة.QTimer
يُنفّذ update_plots
كل 100 ميلي ثانية.setData()
يُستخدم لتحديث البيانات بسرعة.np.concatenate
يُستخدم لجعل الرسم يتدفق بشكل دائري.المشكلة | الحل |
---|---|
الرسومات لا تُحدّث | تأكد من أن timer.start() يعمل |
البيانات تتوقف عند 200 | تأكد من أن ptr يتم تدويره % buffer_size |
الألوان لا تتغير | تأكد من setStyleSheet() |
QLabel لا يعرض القيم |
تأكد من setText() |
المؤشر لا يظهر | تأكد من show() للنافذة |
ImageView
.ImageView
: لعرض مصفوفة حرارة تمثل توزيع الحرارة.GLViewWidget
: لعرض نموذج ثلاثي الأبعاد للمصنع.Circular Buffer
للبيانات الحية.setData()
بدل plot()
في الحلقات.QTimer
بدل time.sleep()
.ptr
في الكود؟QVBoxLayout
و QHBoxLayout
؟QLabel
ديناميكيًا؟np.concatenate
في update_curve
؟العنصر | الوظيفة |
---|---|
QMainWindow |
النافذة الرئيسية |
QTimer |
التحديث اللحظي |
Circular Buffer |
تخزين البيانات |
setData() |
تحديث الرسم بسرعة |
setStyleSheet() |
تغيير مظهر العناصر |
QLabel |
عرض القيم الحالية |
الدالة | الوصف |
---|---|
pg.plot() |
إنشاء رسم سريع |
pg.PlotWidget() |
حاوية رسم |
pg.GraphicsLayoutWidget() |
تخطيط متعدد الرسومات |
curve.setData() |
تحديث بيانات الرسم |
view.setMouseEnabled() |
التحكم في التفاعل |
scatter.sigClicked.connect() |
ربط حدث النقر |
imageView.setImage() |
عرض صورة |
imageView.setColorMap() |
تطبيق خريطة لونية |
GLViewWidget() |
نافذة 3D |
QTimer() |
التحديث اللحظي |