docs:pyqt4

تعليم البرمجة الرسومية في PyQt4


هذا مدخل لطقم PyQt4. وهو مناسب للمبرمجين المبتدئين والمتوسطين. بعد قراءة هذا المدحل، ستتمكن من عمل تطبيقات PyQt4 غير سطحية. قد ترغب في إلقاء نظرة على مقالة تعلم بايثون أيضا.

مقدمة في طقم أدوات PyQt4

حول هذه المقالة

هذه مقدمة عن PyQt4. الهدف منها هو وضع قدمك على أول الطريق مع طقم أدوات PtQt4 تم كتابة المقال وتجربته على لينكس.

حول PyQt

تعتبر PyQt طقم أدوات لعمل تطبيقات رسومية GUI. وهي عبارة عن دمج بين لغة البرمجة بايثون ومكتبة Qt الناجحة وهذه الأخيرة من أقوى المكتبات على وجه البسيطة. الموقع الرسمي لها هو The official home site for PyQt is on www.riverbankcomputing.co.uk ويطورها فيل ثومسون Phil Thompson.

تم تنفيذ PyQt على شكل طقم من عدة وحدات بايثون Python modules. وهي تحتوي أكثر من 300 صنف class 1) و 6000 وظيفة/طريقة function/method. وهي متعددة المنصات وتعمل على كل الأنظمة الرئيسية بما في ذلك يونكس وويندوز وماك. تخضع PyQt لرخصة مزدوجة حيث يمكن للمطورين الاختيار بين GPL والرخصة تجارية. سابقا كانت رخصة GPL متوفرة فقط على يونكس لكن بدأ من الإصدار الرابعة أصبحت GPL متوفرة على كل المنصات المدعومة.

وحيث أن هناك الكثير من الصنوف Classes المتوفرة تم تقسيمها إلى عدة وحدات Modules.

الوحدات المكونة ل PyQt4

الوحدة QtCore تحتوي الوظائف المركزية غير الرسومية. تستخدم هذه الوحدة في التعامل مع الوقت والملفات والأدلة (المجلدات) وأنواع البيانات المختلفة والسيالات streams وروابط URL وأنواع الملفات MIME والعمليات processes and threads.

الوحدة QtGui فتحتوي على المكونات الرسومية وما يتعلق يها من صنوف. وتشتمل على سبيل المثال على الأزرار والنوافذ وأشرطة الأدوات والحالة والزلاقات والصور النقصية والألوان والخطوط …إلخ.

الوحدة QtSVG بها الصنوف اللازمة لعرض محتويات ملفات SVG. وهي لغة وصفية للرسومات المتجهية ثنائية الأبعاد والتطبيقات الرسومية ب XML

الوحدة QtOpenGL تستخدم لتوليد رسومات ثلاثة الأبعاد أو ثنائية باستخدام مكتبة OpenGL. مما يمكننا من دمج Qt مع OpenGL دون عناء.

الوحدة QtNetwork تحتوي على الصنوف اللازمة لبرمجة الشبكات. هذه الصنوف تسمح بكتابة برامج خوادم severs أو مخدومات clients لبروتوكولات TCP/IP و UDP. مما يجعل برمجية الشبكات أسهل وقابل للنقل لمنصات أخرى.

الصنوف في الوحدة QtXml للعمل على ملفات xml. هذه الوحدة تقدم تطبيق ل SAX و DOM.

الوحدة QtSql تقدم صنوف التعامل مع قواعد البيانات.

بايثون

بايثون لغة نصية ناجحة. بدأ تطويرها Guido van Rossum. وقد اطلقت لاول مرة في عام 1991. بايثون استلهمت من ABC و Haskell. وهي لغة برمجة تفسيرية عالية المستوى وعامة الأغراض ومتعددة المنصات. والبعض يحب أن يسميها اللغة الديناميكية. إنها لغة سهلة التعلم. وهي لغة مصغّرة minimalistic. وأحد أوضح مزاياها أنها لا تستعمل الفاصلة المنقوطة ولا الحاصرات. بل تستخدم الإزاحة (المسافة البادئة indentation) الإصدار الأحدث وقت كتابة المقالة هو 2.5 وقد أطلق في أيلول/سبتمبر 2006. ويقوم اليوم عليها مجموعة كبيرة من المتطوعين حول العالم.

مجتمع مبرمجي TIOBE يقدم ترتيب نظري لمستوى استخدام لغات البرمجة المختلفة. جافا تتربع على عرشها بعد أن خلعت سي++ لكن سي++ ستظل حاضرة بثقلها خلال العقود القادمة ولا يوجد ما يهددها. ونرى تخصص لغات البرمجة فلغة جافا للأعمال enterprise وسهولة النقل portability . وسي هي ملكة برمجة النظم (نظم التشغيل وتعريفات وادوات النظام الصغيرة). ولغة PHP تسيطر على مواقع الويب الصغيرة والمتوسطة وجافاسكربت تستخدم في تطبيقات الويب عند المخدوم client

ترتيب اللغات في 2008 TIOBE
المرتبة اللغة التقييم
1 Java 21.571%
2 C 16.178%
3 (Visual) Basic 10.857%
4 C++ 10.057%
5 PHP 9.349%
6 Python 4.975%
7 Perl 4.694%
8 C# 3.697%
9 Ruby 2.920%
10 JavaScript 2.892%

ونلاحظ أن بايثون تحتل المركز السادس (متقدمة مرتبتين على سي#) وقد كانت قبل عام في المركز الثامن.

الأدوات الرسومية في بايثون

لعمل تطبيقات رسومية يختار مبرمجوا بايثون بين ثلاث خيارات محترمة. وهي PyGTK و wxPython و PyQt، وأيها عليك أن تختار يعتمد على الظروف. وهناك خيار آخر اسمه TkInter تجنبه.

أول البرامج على PyQt4

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

مثال بسيط

هذا الكود مبسط جدا. إنه يظهر نافذة صغيرة. نستطيع القيام بالكثير من الأشياء على هذه النافذة، نحجمها أو نكبرها أو نصغرها وهذا يتطلب كتابة كود، لكن هناك شخص ما قد قام بذلك عنا. ولأن هذا يتكرر في كل التطبيقات لا داع لكتابته مرة بعد أخرى فقد تم إبعاد هذا الجزء من الكود عن ناظر المبرمج. مكتبة أدوات PyQt هي مكتبة عليا. فلو كنا نبرمج في مكتبة دنيا لكان الكود التالي مكونا من عشرات الأسطر.

#!/usr/bin/python
 
# simple.py
 
import sys
from PyQt4 import QtGui
 
app = QtGui.QApplication(sys.argv)
 
widget = QtGui.QWidget()
widget.resize(250, 150)
widget.setWindowTitle('simple')
widget.show()
 
sys.exit(app.exec_())

وإليكم شرح الكود

import sys
from PyQt4 import QtGui

هنا قمنا بعملية الاستيراد import اللازمة. فالودجات 2) الرسومية الأساسية موجودة في وحدة QtGui.

app = QtGui.QApplication(sys.argv)

كل تطبيق على PyQt4 يجب أن ينشئ كائن من نوع تطبيق QApplication وهو موجود في وحدة QtGui المعامل sys.argv هي قائمة بالمعاملات التي تمرر للتطبيق عبر سطر الاوامر. وكما نعلم يمكن تنفيذ نصوص بايثون من الصدفة (سطر الأوامر) فهي طريقة نتحكم فيها ببداية البرنامج.

widget = QtGui.QWidget()

الصنف QWidget هو أساس صنوف base class كل الودجات (الكائنات الرسومية) في PyQt4. وهنا قدمنا على استدعاء المشيد constructor الخاص به. والسلوك التلقائي أن لا يحتاج إلى والد. الودجة التي لا والد لها تكون نافذة.

widget.resize(250, 150)

الطريقة resize تحجّم الكائن. فسيكون عرضه 250 وارتفاعه 150 (بيكسل).

 widget.setWindowTitle('simple')

وهنا وضعنا عنوانا لنافذتنا. هذا العنوان يظهر في شريط العنوان.

 widget.show()

الطريقة show تظهر الكائن على الشاشة

sys.exit(app.exec_())

وأخيرا ندخل في الحلقة الرئيسية mainloop للتطبيق. وتبدأ معالجة الأحداث بعد هذه النقطة. الحلقة الرئيسية تستلم الأحداث من نظام النوافذ وترسلها لودجات التطبيق. وتنتهي الحلقة الرئيسية عندما نستدع exit أو عند تدمير الودجة الرئيسية. وقد قمنا باستدعاء sys.exit للتأكد من إنهاء التطبيق بطريقة نظيفة.

وإذا كنت تتساءل لماذا لحقت _ بالطريقة exec ؟ كل شيء له معنى. ببساطة لان exec عبارة عن كلمة مفتاحية في لغة بايثون.

البرنامج البسيط Simple

أيقونة للتطبيق

أيقونة التطبيق (البرنامج) هي صورة صغيرة تعرض في زاوية شريط العنوان. وفي المثال التالي سنريكم طريقة القيام بذلك في PyQt4 سنقدم بعض الطرق الجديدة.

#!/usr/bin/python
 
# icon.py
 
import sys
from PyQt4 import QtGui
 
 
class WinWithIcon(QtGui.QWidget):
    def __init__(self, parent=None):
        QtGui.QWidget.__init__(self, parent)
 
        self.setGeometry(300, 300, 250, 150)
        self.setWindowTitle('Icon')
        self.setWindowIcon(QtGui.QIcon('icons/web.png'))
 
 
app = QtGui.QApplication(sys.argv)
icon = WinWithIcon()
icon.show()
sys.exit(app.exec_())

المثال السابق كتب بطريقة إجرائية. وبايثون تدعم الطريقة الإجرائية Procedural والموجهة للكائنات OOP. البرمجة مع PyQt4 تعني السير بأسلوب OOP.

class WinWithIcon(QtGui.QWidget):
   def __init__(self, parent=None):
      QtGui.QWidget.__init__(self, parent)

أهم 3 عناصر في البرمجة الموجهة للكائنات هي الصنوف classes والبيانات data والطرق methods. وهنا قمنا بعمل صنف جديد اسمه WinWithIcon يرث الصنف QtGui.QWidget وهذا يعني أننا نستدعي الإنشاءين واحد للصنف WinWithIcon والثاني للصنف الذي ورثه.

self.setGeometry(300, 300, 250, 150)
self.setWindowTitle('Icon')
self.setWindowIcon(QtGui.QIcon('icons/web.png'))

كل الصنوف الثلاثة ورثت من QtGui.QWidget. الطريقة setGeometry تقوم بشيئين. تموضع النافذة في مكانها وتحدد حجمها. المعاملان الأولان هما الإحداثيان x و y للنافذة. الثالث هو العرض والرابع هو الارتفاع. الطريقة الأخيرة (أي الدالة الأخيرة) تحدد الأيقونة. لعمل ذلك قمنا بعمل كائن QIcon وهو يحمّل الأيقونة من المسار المعطى.

برنامج له أيقونة

عرض تلميح

نستطيع ان نعرض بالونات المساعدة3) للودجات الخاصة بنا

#!/usr/bin/python
 
# tooltip.py
 
import sys
from PyQt4 import QtGui
 
class Tooltip(QtGui.QWidget):
    def __init__(self, parent=None):
        QtGui.QWidget.__init__(self, parent)
 
        self.setGeometry(300, 300, 250, 150)
        self.setWindowTitle('Tooltip')
 
        self.setToolTip('This is a <b>QWidget</b> widget')
        QtGui.QToolTip.setFont(QtGui.QFont('OldEnglish', 10))
 
app = QtGui.QApplication(sys.argv)
tooltip = Tooltip()
tooltip.show()
app.exec_()

فى هذا المثال سنقوم بعرض نص تلميج على ودجة QWidget

self.setToolTip('This is a <b>QWidget</b> widget')

ننشئ التحديد باستدعاء الطريقة setToolTip. نستطيع عمل نص غني بالتنسيق Rich Text

QtGui.QToolTip.setFont(QtGui.QFont('OldEnglish', 10))

ولأن الخط التلقائي في QToolTip قبيح نغيّره باستدعاء setFont 4)

تلميح Tooltip

غلق نافذة

الطريقة الواضحة لإغلاق نافذة هي عن طريق الضغط على زر إكس x الموجود بشريط العنوان، سنريك فى المثال التالي كيف تقوم بإغلاق نافذتنا برمجيا.

اخترنا المشيد التالي لصنف QPushButton فى مثالنا5)

QPushButton(string text, QWidget parent = None)

تمثل فيه text النص الظاهر وتمثل parent والد الزر parent الذي فيه سيظر الزر وفي مثالنا هو QWidget.

#!/usr/bin/python
 
# quitbutton.py
 
import sys
from PyQt4 import QtGui, QtCore
 
 
class QuitButton(QtGui.QWidget):
    def __init__(self, parent=None):
        QtGui.QWidget.__init__(self, parent)
 
        self.setGeometry(300, 300, 250, 150)
        self.setWindowTitle('QuitButton')
 
        quit = QtGui.QPushButton('Close', self)
        quit.setGeometry(10, 10, 60, 35)
 
        self.connect(quit, QtCore.SIGNAL('clicked()'),
            QtGui.qApp, QtCore.SLOT('quit()'))
 
 
app = QtGui.QApplication(sys.argv)
qb = QuitButton()
qb.show()
sys.exit(app.exec_())
quit = QtGui.QPushButton('Close', self)
quit.setGeometry(10, 10, 60, 35)

انشأنا زر وحددنا موضعه بإستخدام الطريقة setGeometry 6) ، مثلما موضعنا النافذة على الشاشة.

self.connect(quit, QtCore.SIGNAL('clicked()'),
     QtGui.qApp, QtCore.SLOT('quit()'))

معالجة الأحداث فى PyQt4 مبنية على آلية الإشارة والتّلم7) signal/slot إذا ضغطنا على زر تُبث emitted إشارة ‎ clicked() ‎ والتّلم slot الذي تمرر له هي أي تلم مُعرّف في PtQt أو أي كائن قابل للاستدعاء في بايثون python callable. الطريقة ‎ QtCore.QObject.connect() ‎ تربط الإشارة بالتلم. في حالتنا التلم معرف مسبقا في PyQt وهو ‎ quit() ‎ الإتصال سيتم بين كائنين الأول المرسل (وهو الزر) والمستقبل (وهو كائن التطبيق application object)

زر الخروج

صندوق الرسالة

عادة، عند الضغط على علامة إكس x فى شريط العنوان يتم غلق الودجة، أحيانا نرغب في تعديل هذا السلوك. على سبيل المثال، إن كان لدينا ملف مفتوح فى محرر نصوص قمنا ببعض التعديلات عليه؛ عندها علينا عرض رسالة لتأكيد عملية الإغلاق

#!/usr/bin/python
 
# messagebox.py
 
import sys
from PyQt4 import QtGui
 
 
class MessageBox(QtGui.QWidget):
    def __init__(self, parent=None):
        QtGui.QWidget.__init__(self, parent)
 
        self.setGeometry(300, 300, 250, 150)
        self.setWindowTitle('message box')
 
 
    def closeEvent(self, event):
        reply = QtGui.QMessageBox.question(self, 'Message',
            "Are you sure to quit?", QtGui.QMessageBox.Yes, QtGui.QMessageBox.No)
 
        if reply == QtGui.QMessageBox.Yes:
            event.accept()
        else:
            event.ignore()
 
app = QtGui.QApplication(sys.argv)
qb = MessageBox()
qb.show()
sys.exit(app.exec_())

عند غلق الودجة يتم توليد الحدث QcloseEvent. لتعديل سلوك الودجة يجب علينا إعادة تعريف معالج حدث ‎ ()closeEvent

reply = QtGui.QMessageBox.question(self, 'Message',
     "Are you sure to quit?", QtGui.QMessageBox.Yes, QtGui.QMessageBox.No)

نعرض “صندوف رسالة” له زران “نعم” و “لا” وأول معامل هو السلسلة النصية التي تظهر على شريط العنوان والثاني هو محتوى رسالة صندوق الحوار. الرد يوضع في المتغير reply

 if reply == QtGui.QMessageBox.Yes:
     event.accept()
 else:
    event.ignore()

هنا نختبر قيمة الرد فإن كان المستخدم قد اختار “نعم” يتم الموافقة على الحدث مما يؤدي لإغلاق الودجة وإلا يتم تجاهل الحدث.

صندوق Message Box

وضع النافذة فى منتصف الشاشة

السكربت التالي يبين طريقة توسيط النافذة على سطح المكتب

#!/usr/bin/python
 
# center.py
 
import sys
from PyQt4 import QtGui
 
 
class Center(QtGui.QWidget):
    def __init__(self, parent=None):
        QtGui.QWidget.__init__(self, parent)
 
        self.setWindowTitle('center')
        self.resize(250, 150)
        self.center()
 
    def center(self):
        screen = QtGui.QDesktopWidget().screenGeometry()
        size =  self.geometry()
        self.move((screen.width()-size.width())/2, (screen.height()-size.height())/2)
 
 
app = QtGui.QApplication(sys.argv)
qb = Center()
qb.show()
sys.exit(app.exec_())

وشرحه:

self.resize(250, 150)

هنا نقوم بتحجيم الودجة QWidget إلى 250 بكسل عرض و 150 بكسل ارتفاع

screen = QtGui.QDesktopWidget().screenGeometry()

نحصل على الاستبانة الخاصة بالشاشة

size =  self.geometry()

نحصل على الحجم الخاص بالودجة

self.move((screen.width()-size.width())/2, (screen.height()-size.height())/2)

نقوم بنقل النافذة الى منتصف الشاشة بإستخدام الطريقة move

القوائم وشريط الأدوات فى PyQt4

النافذة الرئيسية

الصنف QMainWindow يقدم نافذة التطبيق الأساسية. وهذا يتيح إنشاء هيكل التطبيق بشرط حالة statusbar وشريط الأدوات وشريط القوائم

شريط الحالة

شريط الحالة هو ودجة تستخدم فى عرض معلومات عن حالة البرنامج (مثل “تم حفظ الملف”)

#!/usr/bin/python
 
# statusbar.py 
 
import sys
from PyQt4 import QtGui
 
class MainWindow(QtGui.QMainWindow):
    def __init__(self):
        QtGui.QMainWindow.__init__(self)
 
        self.resize(250, 150)
        self.setWindowTitle('statusbar')
 
        self.statusBar().showMessage('Ready')
 
 
app = QtGui.QApplication(sys.argv)
main = MainWindow()
main.show()
sys.exit(app.exec_())

الشرح:

self.statusBar().showMessage('Ready')

للحصول على شريط الحالة نقوم بإستدعاء الطريقة statusBar ونقوم بإستدعاء الطريقة showMessage لعرض رسالة ما على شريط الحالة

شريط القوائم

شريط القوائم أحد أوضح الأجزاء فى واجهة التطبيق، هو مجموعة من الأوامر commands موزعةً على القوائم. كان لزاما عليك تذكر الأوامر في تطبيقات المحاورة console أما هنا فلا تحتاج لتذكر الأوامر إذ تجمعت إلى أجزاء منطقية. وهناك معايير معتمدة لتقليل الوقت اللازم لتعلم التطبيقات الجديدة.

#!/usr/bin/python
 
# menubar.py 
 
import sys
from PyQt4 import QtGui, QtCore
 
class MainWindow(QtGui.QMainWindow):
    def __init__(self):
        QtGui.QMainWindow.__init__(self)
 
        self.resize(250, 150)
        self.setWindowTitle('menubar')
 
        exit = QtGui.QAction(QtGui.QIcon('icons/exit.png'), 'Exit', self)
        exit.setShortcut('Ctrl+Q')
        exit.setStatusTip('Exit application')
        self.connect(exit, QtCore.SIGNAL('triggered()'), QtCore.SLOT('close()'))
 
        self.statusBar()
 
        menubar = self.menuBar()
        file = menubar.addMenu('&File')
        file.addAction(exit)
 
app = QtGui.QApplication(sys.argv)
main = MainWindow()
main.show()
sys.exit(app.exec_())

الشرح:

menubar = self.menuBar()
file = menubar.addMenu('&File')
file.addAction(exit)

أولا أنشأنا شريط القائمة بالطريقة menuBar من الصنف QMainWindow. ثم أضفنا قائمة له بالطريقة AddMenu. وفي النهاية وصلنا كائن العمل action object لقائمة ملف.

شريط الأدوات

في حين يقوم شريط القوائم بتجميع كل الأوامر المستخدمه فى التطبيق، يتفرد شريط الأدوات بتقديم وصول سريع لأكثرها استخداما.

#!/usr/bin/python
 
# toolbar.py 
 
import sys
from PyQt4 import QtGui, QtCore
 
class MainWindow(QtGui.QMainWindow):
    def __init__(self):
        QtGui.QMainWindow.__init__(self)
 
        self.resize(250, 150)
        self.setWindowTitle('toolbar')
 
        self.exit = QtGui.QAction(QtGui.QIcon('icons/exit.png'), 'Exit', self)
        self.exit.setShortcut('Ctrl+Q')
        self.connect(self.exit, QtCore.SIGNAL('triggered()'), QtCore.SLOT('close()'))
 
        self.toolbar = self.addToolBar('Exit')
        self.toolbar.addAction(self.exit)
 
 
app = QtGui.QApplication(sys.argv)
main = MainWindow()
main.show()
sys.exit(app.exec_())

الشرح:

self.exit = QtGui.QAction(QtGui.QIcon('icons/exit.png'), 'Exit', self)
self.exit.setShortcut('Ctrl+Q')

يُتحكم بتطبيقات الواجهة الرسومية GUI بأوامر، تطلق هذه الأوامر من شريط القوائم أو من القوائم السياقية (بالزر الأيمن مثلا) أو شريط الأدوات أو مفتاح مختصر (CTRL+Q مثلا). تسهّل PyQt التطوير عبر تقديم مفهوم الأعمل actions. العمل قد يكون له نص ليظهر في القائمة وأيقونة ومفتاح مختصر ونص الحالة ونص “ما هذا ؟” ونص التلميح. في مثالنا عرّفنا عمل له أيقونة وتلميح واختصار.

self.connect(self.exit, QtCore.SIGNAL('triggered()'), QtCore.SLOT('close()'))
 

هنا قمنا بربط الإشارة triggered الخاصة بالعمل action إلى التلم المعرف مسبقا close.

  self.toolbar = self.addToolBar('Exit')
  self.toolbar.addAction(self.exit)

وهنا أنشأنا الشرط ووصنا العمل إليه.

pyqt4toolbar.jpg

ضم كل ذلك

في آخر امثلة هذا الفصل سنقوم بإنشاء شريط قوائم وشريط ادوات وشريط حالة وودجة مركزية.

#!/usr/bin/python
 
# mainwindow.py 
 
import sys
from PyQt4 import QtGui, QtCore
 
class MainWindow(QtGui.QMainWindow):
    def __init__(self):
        QtGui.QMainWindow.__init__(self)
 
        self.resize(350, 250)
        self.setWindowTitle('mainwindow')
 
        textEdit = QtGui.QTextEdit()
        self.setCentralWidget(textEdit)
 
        exit = QtGui.QAction(QtGui.QIcon('icons/exit.png'), 'Exit', self)
        exit.setShortcut('Ctrl+Q')
        exit.setStatusTip('Exit application')
        self.connect(exit, QtCore.SIGNAL('triggered()'), QtCore.SLOT('close()'))
 
        self.statusBar()
 
        menubar = self.menuBar()
        file = menubar.addMenu('&File')
        file.addAction(exit)
 
        toolbar = self.addToolBar('Exit')
        toolbar.addAction(exit)
 
 
app = QtGui.QApplication(sys.argv)
main = MainWindow()
main.show()
sys.exit(app.exec_())

الشرح:

  textEdit = QtGui.QTextEdit()
  self.setCentralWidget(textEdit)

هنا نقوم بإضافة ودجة تحرير النصوص ونجعله الودجة المركزية للنافذة QMainWindow، الودجة المركزى سيقوم بشغل كل المساحة المتبقية.

pyqt4mainwindow.jpg

إدارة المخطط فى PyQt4

إن إدارة المخطط (توزيع المساحات في واجهة البرنامج) أمر مهم، فعن طريقها نقوم بتحديد طريقة توزيع الودجات على النافذة (مكانا وحجما).

نستطيع إدارتها بطريقين: الموقع المطلق أو استخدام احد صنوف التخطيط layout classes

الموقع المطلق

وهي أن يحدد المبرمج مكان وحجم كل ودجة بوحدة بكسل، عند استخدامك للتحديد المطلق يجب ان تعلم عدة اشياء:

  • عند اعادة تحجيم النافذة ، لا يغيّر مكان وحجم الودجة تبعا لذلك
  • التطبيق قد يختلف شكله على المنصات المختلفة
  • تغيّر الخطوط قد يفسد تصميم المخطط
  • إذا قررت تغيّر المخطط، فإنك ستحتاج الى إعادة تنظيمه بالكامل وهو أمر ممل ويأخذ وقتا
#!/usr/bin/python
 
# absolute.py
 
import sys
from PyQt4 import QtGui
 
 
class Absolute(QtGui.QWidget):
    def __init__(self, parent=None):
        QtGui.QWidget.__init__(self, parent)
 
        self.setWindowTitle('absolute')
 
        label = QtGui.QLabel('ABC', self)
        label.move(15, 10)
 
        label = QtGui.QLabel('DEF', self)
        label.move(35, 40)
 
        label = QtGui.QLabel('GHI', self)
        label.move(55, 65)
 
        label = QtGui.QLabel('JKL', self)
        label.move(115, 65)
 
        label = QtGui.QLabel('MNO', self)
        label.move(135, 45)
 
        label = QtGui.QLabel('PQR', self)
        label.move(115, 25)
 
        label = QtGui.QLabel('STUV', self)
        label.move(145, 10)
 
        label = QtGui.QLabel('WXYZ', self)
        label.move(215, 10)
 
        self.resize(250, 150)
 
app = QtGui.QApplication(sys.argv)
qb = Absolute()
qb.show()
sys.exit(app.exec_())

ببساطة، نقوم بإستدعاء الطريقة move لتحديد موقع كل ودجة.

نحدد الموقع بالإحاثي السيني x والصادي y. نقطة الأصل في نظام الإحداثيات هي الزاوية العليا اليسرى وتزداد فيه قيم س من اليسار إلى اليمين وقيم ص تزداد من أعلى لأسفل.

المخطط الصندوقي

إدارة المخطط بصفنوف المخططات أكثر مرونة وعملية. وهى الطريقة المفضلة لوضع ودجات على نافذة.

الصفنوف الأساسية هى QHBoxLayout و QVBoxLayout وهما يرصفان الودجات أفقيا أو عاموديا (على الترتيب).

تخيل أننا نريد وضع زرين فى أسفل يمين النافذة. لعمل مثل هذا المخطط سنستخدم صندوق افقى وصندوق رأسى ولإنشاء المساحة الباقية سنستخدم معامل التمدد stretch factor.

#!/usr/bin/python
 
# boxlayout.py
 
import sys
from PyQt4 import QtGui
 
 
class BoxLayout(QtGui.QWidget):
    def __init__(self, parent=None):
        QtGui.QWidget.__init__(self, parent)
 
        self.setWindowTitle('box layout')
 
        ok = QtGui.QPushButton("OK")
        cancel = QtGui.QPushButton("Cancel")
 
        hbox = QtGui.QHBoxLayout()
        hbox.addStretch(1)
        hbox.addWidget(ok)
        hbox.addWidget(cancel)
 
        vbox = QtGui.QVBoxLayout()
        vbox.addStretch(1)
        vbox.addLayout(hbox)
 
        self.setLayout(vbox)
 
        self.resize(300, 150)
 
app = QtGui.QApplication(sys.argv)
qb = BoxLayout()
qb.show()
sys.exit(app.exec_())

الشرح:

ok = QtGui.QPushButton("OK")
cancel = QtGui.QPushButton("Cancel")

هنا نقوم بإنشاء الزرين من الصنف QPushButton ونقوم بتحدد النص الظاهر عليهم كمعاملات للمشيد.

hbox = QtGui.QHBoxLayout()
hbox.addStretch(1)
hbox.addWidget(ok)
hbox.addWidget(cancel)

هنا ننشئ صندوق أفقى، ونضيف معامل التمدد بإستخدام الطريقة addStretch ونقوم بإضافة الودجات بإستخدام الطريقة addWidget

vbox = QtGui.QVBoxLayout()
vbox.addStretch(1)
vbox.addLayout(hbox)

لإنشاء المخطط النهائي المطلوب، نقوم بإنشاء صندوق عمودي ونضع الأفقي فيه.

self.setLayout(vbox)

أخيرا نقوم بتحديد المخطط الرئيسى للنافذة بإستخدام الطريقة setLayout

pyqt4boxlayout.jpg

مخطط الشبكة

اكثر مخطط استخداما هو صنف مخطط الشبكة أو الجدول Grid وفيه يتم تقسيم المساحة إلى صفوف وأعمدة. ونستخدم الصنف QGridLayout لتنفيذ ذلك.

#!/usr/bin/python
 
# gridlayout.py
 
import sys
from PyQt4 import QtGui
 
 
class GridLayout(QtGui.QWidget):
    def __init__(self, parent=None):
        QtGui.QWidget.__init__(self, parent)
 
        self.setWindowTitle('grid layout')
 
        names = ['Cls', 'Bck', '', 'Close', '7', '8', '9', '/',
            '4', '5', '6', '*', '1', '2', '3', '-',
            '0', '.', '=', '+']
 
        grid = QtGui.QGridLayout()
 
        j = 0
        pos = [(0, 0), (0, 1), (0, 2), (0, 3),
                (1, 0), (1, 1), (1, 2), (1, 3),
                (2, 0), (2, 1), (2, 2), (2, 3),
                (3, 0), (3, 1), (3, 2), (3, 3 ),
                (4, 0), (4, 1), (4, 2), (4, 3)]
 
        for i in names:
            button = QtGui.QPushButton(i)
            if j == 2:
                grid.addWidget(QtGui.QLabel(''), 0, 2)
            else: grid.addWidget(button, pos[j][0], pos[j][1])
            j = j + 1
 
        self.setLayout(grid)
 
 
 
app = QtGui.QApplication(sys.argv)
qb = GridLayout()
qb.show()
sys.exit(app.exec_())

فى مثالنا سننشئ شبكة من الأزرار. وأحد الأماكن سنحتاج فيها لترك فراغ8) بالودجة QLabel

grid = QtGui.QGridLayout()

هنا ننشئ كائن مخطط الشبكة باسم grid

 if j == 2:
     grid.addWidget(QtGui.QLabel(''), 0, 2)
 else: grid.addWidget(button, pos[j][0], pos[j][1])

نقوم بإضافة الودجات بإستخدام الطريقة addWidget ومعاملاتها هنا هم (الودجة، رقم الصف، رقم العمود)

pyqt4gridlayout.jpg

الودجات تستطيع شغل أكثر من عمود أو صف في الشبكة، كما فى المثال التالي

#!/usr/bin/python
 
# gridlayout2.py
 
import sys
from PyQt4 import QtGui
 
 
class GridLayout2(QtGui.QWidget):
    def __init__(self, parent=None):
        QtGui.QWidget.__init__(self, parent)
 
        self.setWindowTitle('grid layout')
 
        title = QtGui.QLabel('Title')
        author = QtGui.QLabel('Author')
        review = QtGui.QLabel('Review')
 
        titleEdit = QtGui.QLineEdit()
        authorEdit = QtGui.QLineEdit()
        reviewEdit = QtGui.QTextEdit()
 
        grid = QtGui.QGridLayout()
        grid.setSpacing(10)
 
        grid.addWidget(title, 1, 0)
        grid.addWidget(titleEdit, 1, 1)
 
        grid.addWidget(author, 2, 0)
        grid.addWidget(authorEdit, 2, 1)
 
        grid.addWidget(review, 3, 0)
        grid.addWidget(reviewEdit, 3, 1, 5, 1)
 
 
        self.setLayout(grid)
        self.resize(350, 300)
 
 
app = QtGui.QApplication(sys.argv)
qb = GridLayout2()
qb.show()
sys.exit(app.exec_())

الشرح

grid = QtGui.QGridLayout()
grid.setSpacing(10)

قمنا بإنشاء grid و بتحديد المسافة بين الودجات بإستخدام الطريقة setSpacing

grid.addWidget(reviewEdit, 3, 1, 5, 1)

إذا قمنا بإضافة ودجة إلى شبكة، نستطيع ان نقوم بتحديد التمدد على الصفوف أو الأعمدة. فى حالتنا هذه قمنا بمد الودجة reviewEdit إلى 5 صفوف

pyqt4gridlayout2.jpg

الأحداث والإشارات فى PyQt4

فى هذا الجزء سنستكشف للأحداث والإشارات اللتي تحدث فى التطبيقات.

الأحداث

الأحداث هى جزء هام في أي تطبيق رسومى، وهى تحدث من المستخدم او من النظام. عندما نستدعى طريقة التطبيق ‎ exec_() ‎ يدخل التطبيق فى الحلقة الأساسية main loop. وهي بدورها تستخرج الأحداث وترسلها إلى الكائنات. قدمت Trolltech (الآن جزء من Nokia) بتقديم آلية فريدة هي الإشارات والأتلام

الإشارات والأتلام Signals & Slots

الإشارات تحدث عندما يضغط مستخدم على زر او يحرك منزلق .. إلخ، وقد تحدث من البيئة (على سبيل المثال دقات الساعة)

التلم هو طريقة method (الطريقة مصطلح يكافئ الوظيفة أو الدالة في البرمجة الإجرائية) تتفاعل ردا على إشارة. ويمكن أن يكون التلم أي كائن قابل للاستدعاء في بايثون.

#!/usr/bin/python
 
# sigslot.py
 
import sys
from PyQt4 import QtGui, QtCore
 
 
class SigSlot(QtGui.QWidget):
    def __init__(self, parent=None):
        QtGui.QWidget.__init__(self, parent)
 
        self.setWindowTitle('signal & slot')
 
        lcd = QtGui.QLCDNumber(self)
        slider = QtGui.QSlider(QtCore.Qt.Horizontal, self)
 
        vbox = QtGui.QVBoxLayout()
        vbox.addWidget(lcd)
        vbox.addWidget(slider)
 
        self.setLayout(vbox)
        self.connect(slider,  QtCore.SIGNAL('valueChanged(int)'), lcd, 
		QtCore.SLOT('display(int)') )
 
        self.resize(250, 150)
 
 
app = QtGui.QApplication(sys.argv)
qb = SigSlot()
qb.show()
sys.exit(app.exec_())

فى مثالنا، قمنا بعرض رقم LCD مع منزلق. ونقوم بتغيير الرقم بناء على سحب المنزلق

self.connect(slider,  QtCore.SIGNAL('valueChanged(int)'), lcd, QtCore.SLOT('display(int)') )

هنا قمنا بربط الإشارة valueChanged الخاصة بالمنزلق بالتلم display الخاصة بال LCD للربط يوجد 4 معاملات المرسل وهو الكائن اللذي أرسل الإشارة ثم الإشارة ثم المستقبل للإشارة ثم أخيرا التلم وهى التي ستتفاعل مع الإشارة.

pyqt4signalslot.jpg

تعديل سلوك معالج الحدث

الأحداث في PyQt يتم معالجتها غالبا بإعادة تعريف “معالج الحدث” event handlers (وهي طريقة يتم تنفيذها لمعالجة حدث ما)

#!/usr/bin/python
 
# escape.py
 
import sys
from PyQt4 import QtGui, QtCore
 
class Escape(QtGui.QWidget):
    def __init__(self, parent=None):
        QtGui.QWidget.__init__(self, parent)
 
        self.setWindowTitle('escape')
        self.resize(250, 150)
 
    def keyPressEvent(self, event):
        if event.key() == QtCore.Qt.Key_Escape:
            self.close()
 
app = QtGui.QApplication(sys.argv)
qb = Escape()
qb.show()
sys.exit(app.exec_())

فى مثالنا، ثمنا بإعادة تعريف معالج الحدث keyPressEvent

 def keyPressEvent(self, event):
     if event.key() == QtCore.Qt.Key_Escape:
         self.close()

إذا تم الضغط على زر escape فيتم الخروج من التطبيق

بث الإشارات

الكائنات المنشئة من الصنف QtCore.QObject (أو أي صنف يرثها) تستطيع بت إشارات، اذا ضغطنا على زر ما فيتم توليد الإشارة clicked. في المثال التالي سنرى كيف يتم إرسال الإشارات

#!/usr/bin/python
 
# emit.py
 
import sys
from PyQt4 import QtGui, QtCore
 
 
class Emit(QtGui.QWidget):
    def __init__(self, parent=None):
        QtGui.QWidget.__init__(self, parent)
 
        self.setWindowTitle('emit')
        self.resize(250, 150)
        self.connect(self, QtCore.SIGNAL('closeEmitApp()'), QtCore.SLOT('close()') )
 
    def mousePressEvent(self, event):
        self.emit(QtCore.SIGNAL('closeEmitApp()'))
 
app = QtGui.QApplication(sys.argv)
qb = Emit()
qb.show()
sys.exit(app.exec_())

انشأنا إشارة باسم closeEmitApp وسيتم بثها عند الضغط بالفأرة

def mousePressEvent(self, event): self.emit(QtCore.SIGNAL('closeEmitApp()'))

نقوم ببث الإشارة عبر الطريقة emit وهى تأخذ تعريف الإشارة كمعامل لها

self.connect(self, QtCore.SIGNAL('closeEmitApp()'), QtCore.SLOT('close()') )

هنا قمنا بربط الإشارة closeEmitApp بالتلم close

صناديق الحوار فى PyQt4

نوافذ الحوار Dialog مكون حيوي في التطبيقات الرسومية، الحوار هو محادثة بين شخصين أو أكثر، في التطبيقات الحاسوب فإن صندوق الحوار نافذة من خلالها نتحدث مع التطبيق. يمكن استخدامها فى إدخال بيانات وتعديلها أو التحكم فى إعدادات البرنامج.. الخ. صناديق الحوار وسيلة مهمة في التواصل بين المستخدم والحاسوب.

هناك نوعان منها: المعرفة مسبقا أو المخصصة.

صناديق حوار معرفة مسبقا

الصنف QInputDialog

QinputDialog يقدم طريقة مريحة للحصول على قيمة من المستخدم (سلسلة نصية ، رقم ، عنصر من قائمة)

#!/usr/bin/python
 
# inputdialog.py
 
import sys
from PyQt4 import QtGui
from PyQt4 import QtCore
 
class InputDialog(QtGui.QWidget):
    def __init__(self, parent=None):
        QtGui.QWidget.__init__(self, parent)
 
        self.setGeometry(300, 300, 350, 80)
        self.setWindowTitle('InputDialog')
 
        self.button = QtGui.QPushButton('Dialog', self)
        self.button.setFocusPolicy(QtCore.Qt.NoFocus)
 
        self.button.move(20, 20)
        self.connect(self.button, QtCore.SIGNAL('clicked()'), self.showDialog)
        self.setFocus()
 
        self.label = QtGui.QLineEdit(self)
        self.label.move(130, 22)
 
 
    def showDialog(self):
        text, ok = QtGui.QInputDialog.getText(self, 'Input Dialog', 'Enter your name:')
 
        if ok:
            self.label.setText(unicode(text))
 
 
app = QtGui.QApplication(sys.argv)
icon = InputDialog()
icon.show()
app.exec_()

هذا المثال يشمل زر و line edit. الزر سيقوم بعرض InputDialog للحصول على قيمة، ووضعها فى ال line edit

text, ok = QtGui.QInputDialog.getText(self, 'Input Dialog', 'Enter your name:')

هذا السطر يعرض ال input dialog. ,وذلك بإستخدام الطريقة getText ومعاملاته كالتالى: الوالد، العنوان، الرسالة .. يعيد هذا ال dialog النص المدخل وقيمة منطقية (مرتبطة بالزر الذى ضغطه المستخدم اذا كان زر OK فإن القيمة ستكون True وإلا فإنها False.

pyqt4inputdialog.jpg

الصنف QColorDialog

الصنف QColorDialog يتيح لنا تحديد خطوط ما (اختيار خط)

#!/usr/bin/python
 
# colordialog.py
 
import sys
from PyQt4 import QtGui
from PyQt4 import QtCore
 
 
class ColorDialog(QtGui.QWidget):
    def __init__(self, parent=None):
        QtGui.QWidget.__init__(self, parent)
 
        color = QtGui.QColor(0, 0, 0)
 
        self.setGeometry(300, 300, 250, 180)
        self.setWindowTitle('ColorDialog')
 
        self.button = QtGui.QPushButton('Dialog', self)
        self.button.setFocusPolicy(QtCore.Qt.NoFocus)
        self.button.move(20, 20)
 
        self.connect(self.button, QtCore.SIGNAL('clicked()'), self.showDialog)
        self.setFocus()
 
        self.widget = QtGui.QWidget(self)
        self.widget.setStyleSheet("QWidget { background-color: %s }"
            % color.name())
        self.widget.setGeometry(130, 22, 100, 100)
 
 
    def showDialog(self):
        color = QtGui.QColorDialog.getColor()
 
        self.widget.setStyleSheet("QWidget { background-color: %s }"
            % color.name())
 
app = QtGui.QApplication(sys.argv)
cd = ColorDialog()
cd.show()
app.exec_()

T هذا المثال يعرض زر وودجة ذو خلفية سوداء. سنقوم بتحديد الخلفية عن طريق الناتج من استدعاء الطريقة QColorDialog.getColor

color = QtGui.QColorDialog.getColor()

هذا السطر سيقوم بعرض ال QColorDialog وذلك بإستدعاء الطريقة getColor للحصول على لون ما من المستخدم لتغيير لون خلفية الودجة self.widget إليه وذلك بإستخدام الطريقة setStyleSheet

self.widget.setStyleSheet("QWidget { background-color: %s }" % color.name())

We change the background color using stylesheets.

pyqt4colordialog.jpg

الصنف QFontDialog

QFontDialog هو صف لإختيار الخطوط

#!/usr/bin/python
 
# fontdialog.py
 
import sys
from PyQt4 import QtGui
from PyQt4 import QtCore
 
 
class FontDialog(QtGui.QWidget):
    def __init__(self, parent=None):
        QtGui.QWidget.__init__(self, parent)
 
        hbox = QtGui.QHBoxLayout()
 
        self.setGeometry(300, 300, 250, 110)
        self.setWindowTitle('FontDialog')
 
        button = QtGui.QPushButton('Dialog', self)
        button.setFocusPolicy(QtCore.Qt.NoFocus)
        button.move(20, 20)
 
        hbox.addWidget(button)
 
        self.connect(button, QtCore.SIGNAL('clicked()'), self.showDialog)
 
        self.label = QtGui.QLabel('Knowledge only matters', self)
        self.label.move(130, 20)
 
        hbox.addWidget(self.label, 1)
        self.setLayout(hbox)
 
 
    def showDialog(self):
        font, ok = QtGui.QFontDialog.getFont()
        if ok:
            self.label.setFont(font)
 
 
app = QtGui.QApplication(sys.argv)
cd = FontDialog()
cd.show()
app.exec_()

فى مثالنا لدينا زر ونص الساكن ( label) وبإستخدام الصنف QFontDialog نقوم بتغيير الخط على ذلك النص الثابت

hbox.addWidget(self.label, 1)

هنا نجعل ال self.label قابل للتحجيم، وذلك لمرونة التعامل مع الخطوط وإلا قد لايظهر النص على ال self.label بصورة كاملة

font, ok = QtGui.QFontDialog.getFont()

نستخدم الطريقة getFont لعرض الdialog للحصول على الخط

if ok: self.label.setFont(font)

اذا قام المستخدم بضغط الزر OK على الdialog سيتم تغيير الخط على ال self.label “النص الساكن“

pyqt4fontdialog.jpg

الصنف QFileDialog

الصنف QFileDialog يتيح لنا اختيار ملفات او مجلدات (سواء للفتح او للحفظ)

#!/usr/bin/python
 
# openfiledialog.py
 
import sys
from PyQt4 import QtGui
from PyQt4 import QtCore
 
 
class OpenFile(QtGui.QMainWindow):
    def __init__(self, parent=None):
        QtGui.QMainWindow.__init__(self, parent)
 
        self.setGeometry(300, 300, 350, 300)
        self.setWindowTitle('OpenFile')
 
        self.textEdit = QtGui.QTextEdit()
        self.setCentralWidget(self.textEdit)
        self.statusBar()
        self.setFocus()
 
        exit = QtGui.QAction(QtGui.QIcon('open.png'), 'Open', self)
        exit.setShortcut('Ctrl+O')
        exit.setStatusTip('Open new File')
        self.connect(exit, QtCore.SIGNAL('triggered()'), self.showDialog)
 
        menubar = self.menuBar()
        file = menubar.addMenu('&File')
        file.addAction(exit)
 
    def showDialog(self):
        filename = QtGui.QFileDialog.getOpenFileName(self, 'Open file',
                    '/home')
        file=open(filename)
        data = file.read()
        self.textEdit.setText(data)
 
app = QtGui.QApplication(sys.argv)
cd = OpenFile()
cd.show()
app.exec_()

Th هذا المثال يعرض شريط وقوائم وودجة مركزى TextEdit “ودجة النصوص” وشريط الحالة.

عنصر القائمة يقوم بعرض QFileDialog لإختيار ملف ما، وعرض محتواه على ودجة النصوص

class OpenFile(QtGui.QMainWindow):
...
        self.textEdit = QtGui.QTextEdit()
        self.setCentralWidget(self.textEdit)

هذا المثال مبنى على الصنف QMainWidget وذلك لتحديد الودجة المركزى وذلك يتم بسهولة مع الصنف QMainWindow

filename = QtGui.QFileDialog.getOpenFileName(self, 'Open file', '/home')

هنا نقوم بعرض ال QFileDialog للحصول على ملف ما للفتح بإستخدام الطريقة getOpenFileName ولها معاملات(الأب ، العنوان، ومسار البداية)، والفلتر المستخدم افتراضيا هو كل الملفات (*)

file=open(filename)
data = file.read()
self.textEdit.setText(data)

يتم قراءة الملف بإستخدام الطريقة read الخاصة بال file object ويتم عرض المحتوى بإستخدام الطريقة setText الخاصة بالودجة textEdit

pyqt4filedialog.jpg

الودجات

الودجات هى وحدات البناء للتطبيق، وتقدم لنا PyQt عدد كبير من الودجات المختلفة كالأزرار والمنزلقات والصناديق المنطقية والقوائم.. الخ وكل مايحتاجه المبرمج لعمله. سنقوم بالتعرض لبعض الودجات المفيدة فى هذه الجزئية

الصنف QCheckBox

QCheckBox هو ودجة بحالتين on “منشط” او off “غير منشط“ ويشمل نص للوصف.عند اى تغيير فى الحالة يتم ارسال الإشارة statChanged()

#!/usr/bin/python
 
# checkbox.py
 
import sys
from PyQt4 import QtGui
from PyQt4 import QtCore
 
 
class CheckBox(QtGui.QWidget):
    def __init__(self, parent=None):
        QtGui.QWidget.__init__(self, parent)
 
        self.setGeometry(300, 300, 250, 150)
        self.setWindowTitle('Checkbox')
 
        self.cb = QtGui.QCheckBox('Show title', self)
        self.cb.setFocusPolicy(QtCore.Qt.NoFocus)
        self.cb.move(10, 10)
        self.cb.toggle();
        self.connect(self.cb, QtCore.SIGNAL('stateChanged(int)'), self.changeTitle)
 
    def changeTitle(self, value):
        if self.cb.isChecked():
            self.setWindowTitle('Checkbox')
        else:
            self.setWindowTitle('')
 
app = QtGui.QApplication(sys.argv)
icon = CheckBox()
icon.show()
app.exec_()

فى مثالنا سننشئ اوبجكت من ال CheckBox ليتحكم فى عرض عنوان النافذة.

self.cb = QtGui.QCheckBox('Show title', self)

هذا هو المشيد الخاص بال QCheckBox وفيه النص المرفق معه والأب

self.cb.setFocusPolicy(QtCore.Qt.NoFocus)

الصنف QCheckBox بيقبل حدوث تركيز عليه. قمت هنا بمنع ذلك لمنع ظهور ذلك المستطيل البشع!

نربط السلوت “الطريقة اللتى سنقوم بتعريفها changeTitle” بإشارة تغيير الحالة stateChanged

self.connect(self.cb, QtCore.SIGNAL('stateChanged(int)'), self.changeTitle)
    def changeTitle(self, value):
        if self.cb.isChecked():
            self.setWindowTitle('Checkbox')
        else:
            self.setWindowTitle('')

قمنا بعكس الحالة الخاصة بال checkbox بإستخدام الطريقة toggle()، افتراضيا عنوان النافذة غير ظاهر وال checkbox غير منشط

self.cb.toggle();

We set the window title, so we must also check the checkbox. By default, the window title is not set and the check box is unchecked.

pyqt4checkbox.jpg

الصنف ToggleButton

PyQt لاتشمل ToggleButton “زر بحالتين مشابه لل CheckBox”، لذا لإنشاءه بنستخدم الصنف QPushButton بطريقة خاصة.

#!/usr/bin/python
 
# togglebutton.py
 
import sys
from PyQt4 import QtGui
from PyQt4 import QtCore
 
 
class ToggleButton(QtGui.QWidget):
    def __init__(self, parent=None):
        QtGui.QWidget.__init__(self, parent)
 
        self.color = QtGui.QColor(0, 0, 0) 
 
        self.setGeometry(300, 300, 280, 170)
        self.setWindowTitle('ToggleButton')
 
        self.red = QtGui.QPushButton('Red', self)
        self.red.setCheckable(True)
        self.red.move(10, 10)
 
        self.connect(self.red, QtCore.SIGNAL('clicked()'), self.setRed)
 
        self.green = QtGui.QPushButton('Green', self)
        self.green.setCheckable(True)
        self.green.move(10, 60)
 
        self.connect(self.green, QtCore.SIGNAL('clicked()'), self.setGreen)
 
        self.blue = QtGui.QPushButton('Blue', self)
        self.blue.setCheckable(True)
        self.blue.move(10, 110)
 
        self.connect(self.blue, QtCore.SIGNAL('clicked()'), self.setBlue)
 
        self.square = QtGui.QWidget(self)
        self.square.setGeometry(150, 20, 100, 100)
        self.square.setStyleSheet("QWidget { background-color: %s }" % self.color.name())
 
        QtGui.QApplication.setStyle(QtGui.QStyleFactory.create('cleanlooks'))
 
    def setRed(self):
        if self.red.isChecked():
            self.color.setRed(255)
        else: self.color.setRed(0)
 
        self.square.setStyleSheet("QWidget { background-color: %s }" % self.color.name())
 
    def setGreen(self):
        if self.green.isChecked():
            self.color.setGreen(255)
        else: self.color.setGreen(0)
 
        self.square.setStyleSheet("QWidget { background-color: %s }" % self.color.name())
 
    def setBlue(self):
        if self.blue.isChecked():
            self.color.setBlue(255)
        else: self.color.setBlue(0)
 
        self.square.setStyleSheet("QWidget { background-color: %s }" % self.color.name())
 
 
 
app = QtGui.QApplication(sys.argv)
tb = ToggleButton()
tb.show()
app.exec_()

فى مثالنا قمنا بإنشاء ثلاثة ToggleButtons وانشئنا ودجة وحددنا خلفيته باللون الأسود وال3 ازرار سيقومو بعكس الحالة بين الألوان الأحمر والأخضر والأزرق

self.color = QtGui.QColor(0, 0, 0)

Tهذا هو اللون الأساسى “اسود” “لا أحمر ولا أخضر ولاأزرق”

self.red = QtGui.QPushButton('Red', self)
self.red.setCheckable(True)

لإنشاء Toggle Button نقوم بإنشاء زر عادى من الصنف QPushButton ولكن نقوم بتحديد قابليته للتنشيط بإستخدام الطريقة setCheckable

self.connect(self.red, QtCore.SIGNAL('clicked()'), self.setRed)

ونقوم بربط الإشارة clicked بطريقتنا المعرفة setRed

QtGui.QApplication.setStyle(QtGui.QStyleFactory.create('cleanlooks'))

هنا قمنا بتحديد الإستايل الخاص ل cleanlooks وذلك بإستخدام الطريقة setStyle بالتطبيق بسبب عدم وضوع شكل ال Toggle Button على plastique

if self.red.isChecked(): self.color.setRed(255)
else: self.color.setRed(0)

هنا نختبر اذا كان الزر الخاص باللون الأحمر منشط او لا بإستخدام الطريقة isChecked ونقوم بتحديد اللون

self.square.setStyleSheet("QWidget { background-color: %s }" % self.color.name())

قمنا بتغيير لون خلفية الودجة بإستخدام الطريقة setStyleSheet مع تحديد background-color كخاصية ليتم تعديلها

وهكذا مع باقى الأزرار

pyqt4togglebutton.jpg

الصنوف QSlider, QLabel

المنزلق هو ودجة مع مقبض بسيط يمكن سحبه للأمام او للخلف، وذلك لإختيار قيمة ما. قد يكون استخدامه فى بعض الأحيان طبيعيا اكثر من ادخال رقم او استخدام spin box “ودجة مع اسهم لأعلى واسفل لزيادة القيمة وتقليلها”

ال QLabel بيستخدم فى عرض نص ساكن او ربما صورة.

فى مثالنا سنعرض منزلق مع label “نعرض عليه صورة” وسيتم التحكم فيها بإستخدام المنزلق

#!/usr/bin/python
 
# slider-label.py
 
import sys
from PyQt4 import QtGui
from PyQt4 import QtCore
 
class SliderLabel(QtGui.QWidget):
    def __init__(self, parent=None):
        QtGui.QWidget.__init__(self, parent)
 
        self.setGeometry(300, 300, 250, 150)
        self.setWindowTitle('SliderLabel')
 
        self.slider = QtGui.QSlider(QtCore.Qt.Horizontal, self)
        self.slider.setFocusPolicy(QtCore.Qt.NoFocus)
        self.slider.setGeometry(30, 40, 100, 30)
        self.connect(self.slider, QtCore.SIGNAL('valueChanged(int)'), self.changeValue)
 
 
        self.label = QtGui.QLabel(self)
        self.label.setPixmap(QtGui.QPixmap('mute.png'))
        self.label.setGeometry(160, 40, 80, 30)
 
 
    def changeValue(self, value):
        pos = self.slider.value()
 
        if pos == 0:
            self.label.setPixmap(QtGui.QPixmap('mute.png'))
        elif pos > 0 and pos <= 30:
            self.label.setPixmap(QtGui.QPixmap('min.png'))
        elif pos > 30 and pos < 80:
            self.label.setPixmap(QtGui.QPixmap('med.png'))
        else:
            self.label.setPixmap(QtGui.QPixmap('max.png'))
 
app = QtGui.QApplication(sys.argv)
icon = SliderLabel()
icon.show()
app.exec_()

فى مثالنا نحاكى اداة التحكم فى الصوت بجذب المقبض الخاص بالمنزل فيتم تغيير الصورة

self.slider = QtGui.QSlider(QtCore.Qt.Horizontal, self)

قمنا بإنشاء منزلق افقى

self.label = QtGui.QLabel(self)
self.label.setPixmap(QtGui.QPixmap('mute.png'))

انشأنا Label وحددنا الصورة الظاهرة “بصوت صامت” بإستخدام الطريقة setPixmap التى تأخذ اوبجكت من الصنف QPixmap “المشيد له يحوى مسار الصورة“

self.connect(self.slider, QtCore.SIGNAL('valueChanged(int)'), self.changeValue)

هنا قمنا بربط اشارة التغيير فى حالة المنزلق valueChanged الى الطريقة اللتى عرفناها changeValue

    def changeValue(self, value):
        pos = self.slider.value()
 
        if pos == 0:
            self.label.setPixmap(QtGui.QPixmap('mute.png'))
        elif pos > 0 and pos <= 30:
            self.label.setPixmap(QtGui.QPixmap('min.png'))
        elif pos > 30 and pos < 80:
            self.label.setPixmap(QtGui.QPixmap('med.png'))
        else:
            self.label.setPixmap(QtGui.QPixmap('max.png'))
pos = self.slider.value()

هنا نحصل على القيمة الحالية للمنزلق بإستخدام الطريقة value ونغير الصورة بناء عليها. pyqt4sliderlabel.jpg

الصنف QProgressBar

الصنف QProgressBar هو ودجة يستخدم فى تتبع مدى انتهاء المهمات (مفيدة للمستخدم)، قد يكون افقى او رأسى. يتم تقسيم المهمة الى خطوات يقوم المبرمج بتحديد اقل قيمة واعلى قيمة (الإفتراضى هو 0و 99)

#!/usr/bin/python
 
# progressbar.py
 
import sys
from PyQt4 import QtGui
from PyQt4 import QtCore
 
 
class ProgressBar(QtGui.QWidget):
    def __init__(self, parent=None):
        QtGui.QWidget.__init__(self, parent)
 
        self.setGeometry(300, 300, 250, 150)
        self.setWindowTitle('ProgressBar')
 
        self.pbar = QtGui.QProgressBar(self)
        self.pbar.setGeometry(30, 40, 200, 25)
 
        self.button = QtGui.QPushButton('Start', self)
        self.button.setFocusPolicy(QtCore.Qt.NoFocus)
        self.button.move(40, 80)
 
        self.connect(self.button, QtCore.SIGNAL('clicked()'), self.onStart)
 
        self.timer = QtCore.QBasicTimer()
        self.step = 0;
 
 
    def timerEvent(self, event):
        if self.step >= 100:
            self.timer.stop()
            return
        self.step = self.step + 1
        self.pbar.setValue(self.step)
 
    def onStart(self):
        if self.timer.isActive():
            self.timer.stop()
            self.button.setText('Start')
        else:
            self.timer.start(100, self)
            self.button.setText('Stop')
 
 
app = QtGui.QApplication(sys.argv)
icon = ProgressBar()
icon.show()
app.exec_()

فى مثالنا لدينا progress bar افقى مع زر يتحكم فىه “ايقاف واستمرار”

self.pbar = QtGui.QProgressBar(self)

المشيد الخاص بالQProgressBar

self.timer = QtCore.QBasicTimer()

لتنشيط ال progressbar نستخدم مؤقت

self.timer.start(100, self)

هنا نقوم بتحديد ال timeout والمستقبل للحدث بإستخدام الطريقة start

 def timerEvent(self, event):
     if self.step >= 100:
         self.timer.stop()
         return
     self.step = self.step + 1
     self.pbar.setValue(self.step)

لكل مايرث QObject له معالج حدث timerEvent وللتعامل مع ذلك الحدث نقوم بإعادة تعريفه

pyqt4progressbar.jpg

الصنف QCalendarWidget

QCalendarWidget تستخدم لتوفير نتيجة شهرية، تتيح للمستخدم اختيار تاريخ ما

#!/usr/bin/python
 
# calendar.py
 
import sys
from PyQt4 import QtGui
from PyQt4 import QtCore
 
 
class Calendar(QtGui.QWidget):
    def __init__(self, parent=None):
        QtGui.QWidget.__init__(self, parent)
 
        self.setGeometry(300, 300, 350, 300)
        self.setWindowTitle('Calendar')
 
        self.cal = QtGui.QCalendarWidget(self)
        self.cal.setGridVisible(True)
        self.cal.move(20, 20)
        self.connect(self.cal, QtCore.SIGNAL('selectionChanged()'), self.showDate)
 
 
        self.label = QtGui.QLabel(self)
        date = self.cal.selectedDate()
        self.label.setText(str(date.toPyDate()))
        self.label.move(130, 260)
 
 
    def showDate(self):
        date = self.cal.selectedDate()
        self.label.setText(str(date.toPyDate()))
 
 
app = QtGui.QApplication(sys.argv)
icon = Calendar()
icon.show()
app.exec_()

هذا المثال يشمل نتيجة وlabel والتاريخ المختار يتم عرضه على ذلك ال label

self.cal = QtGui.QCalendarWidget(self)

ننشئ اوبجكت من QCalendarWidget

self.connect(self.cal, QtCore.SIGNAL('selectionChanged()'), self.showDate)

إذا قمنا بإختيار تاريخ ما فسيتم ارسال الإشارة selectionChanged ، لذا سنقوم بربطها بطريقة showData التى سنعرفها لعرض التاريخ على ال label

 def showDate(self):
     date = self.cal.selectedDate()
     self.label.setText(str(date.toPyDate()))

هنا حصلنا على التاريخ بإستخدام الطريقة selectedDate وقمنا بتحويله إلى سلسلة نصية بإستخدام str pyqt4calendar.jpg

السحب والإفلات

فى هذه الجزئية سنتحدث عن السحب والإفلات

In computer graphical user interfaces, drag-and-drop is the action of (or support for the action of) clicking on a virtual object and dragging it to a different location or onto another virtual object. In general, it can be used to invoke many kinds of actions, or create various types of associations between two abstract objects. (Wikipedia)

السحب والإفلات احد اوضح جوانب الواجهات الرسومية. عمليات السحب والإفلات تتيح عمل اشياء معقدة

عادة نستطيع ان نسحب ونفلت نوعين بيانات او بعض الأوبجكتس الرسومية. اذا سحبنا صورة من تطبيق لآخر فإننا نقوم بسحب وإفلات بيانات binary اذا قمنا بسحب نافذة فى متصفح فايرفوكس من مكان لآخر فإننا نقوم بسحب مكون رسومى

سحب وإفلات بسيط

فى هذا المثال لدينا QLineEdit و QPushButton اوبجكتس. سنقوم بحسب النص على ودجة النص وإفلاته على الزر

In the first example, we will have a QLineEdit and a QPushButton. We will drag plain text from the line edit widget and drop it onto the button widget.

#!/usr/bin/python
 
# dragdrop.py
 
import sys
from PyQt4 import QtGui
 
class Button(QtGui.QPushButton):
    def __init__(self, title, parent):
        QtGui.QPushButton.__init__(self, title, parent)
        self.setAcceptDrops(True)
 
    def dragEnterEvent(self, event):
        if event.mimeData().hasFormat('text/plain'):
            event.accept()
        else:
            event.ignore() 
 
    def dropEvent(self, event):
	    self.setText(event.mimeData().text()) 
 
 
class DragDrop(QtGui.QDialog):
    def __init__(self, parent=None):
        QtGui.QDialog.__init__(self, parent)
 
        self.resize(280, 150)
        self.setWindowTitle('Simple Drag & Drop')
 
        edit = QtGui.QLineEdit('', self)
	edit.setDragEnabled(True)
        edit.move(30, 65)
 
        button = Button("Button", self)
        button.move(170, 65)
 
 
	screen = QtGui.QDesktopWidget().screenGeometry()
        size =  self.geometry()
        self.move((screen.width()-size.width())/2, 
            (screen.height()-size.height())/2)
 
app = QtGui.QApplication(sys.argv)
icon = DragDrop()
icon.show()
app.exec_()
 class Button(QtGui.QPushButton):
     def __init__(self, title, parent):
         QtGui.QPushButton.__init__(self, title, parent)

لإتاحة الإفلات على الزر يجب علينا اعادة تعريف بعض الطرق فى الصنف الخاص بنا الذى سيرث من الصنف QPushButton

self.setAcceptDrops(True)

هنا نتيج امكانيه احداث الإفلات على للصف

 def dragEnterEvent(self, event):
     if event.mimeData().hasFormat('text/plain'):
         event.accept()
     else:
         event.ignore() 

اولا نعيد تعريف معالج الحدث dragEnterEvent() لنحدد البيانات اللتى سنقبلها، فى حالتنا هى مجرد نص عادى.

 def dropEvent(self, event):
     self.setText(event.mimeData().text()) 

بإعادة تعريف معالج الحدث dropEvent() سنقوم بتعريف ماذا سنفعل اثناء الإفلات. هنا سنقوم بتغير النص الظاهر على الزر.

 edit = QtGui.QLineEdit('', self)
 edit.setDragEnabled(True)

الصنف QLineEdit لديه دعم داخلى لعمليات السحب. كل ماعلينا هو اتاحتها بإستخدام الطريقة setDragEnabled

سحب وإفلات زر

فى المثال التالى سنتعرض لكيفية سحب وإفلات زر

#!/usr/bin/python
 
# dragbutton.py
 
import sys
from PyQt4 import QtGui
from PyQt4 import QtCore
 
class Button(QtGui.QPushButton):
    def __init__(self, title, parent):
        QtGui.QPushButton.__init__(self, title, parent)
 
    def mouseMoveEvent(self, event):
 
        if event.buttons() != QtCore.Qt.RightButton:
	    return
 
        mimeData = QtCore.QMimeData()
 
        drag = QtGui.QDrag(self)
        drag.setMimeData(mimeData)
        drag.setHotSpot(event.pos() - self.rect().topLeft())
 
        dropAction = drag.start(QtCore.Qt.MoveAction)
 
        if dropAction == QtCore.Qt.MoveAction:
            self.close()
 
    def mousePressEvent(self, event):
        QtGui.QPushButton.mousePressEvent(self, event)
        if event.button() == QtCore.Qt.LeftButton:
	    print 'press'
 
 
 
class DragButton(QtGui.QDialog):
    def __init__(self, parent=None):
        QtGui.QDialog.__init__(self, parent)
 
        self.resize(280, 150)
        self.setWindowTitle('Click or Move')
	self.setAcceptDrops(True)
 
        self.button = Button('Close', self)
        self.button.move(100, 65)
 
 
	screen = QtGui.QDesktopWidget().screenGeometry()
        size =  self.geometry()
        self.move((screen.width()-size.width())/2, 
            (screen.height()-size.height())/2)
 
 
    def dragEnterEvent(self, event):
	 event.accept()
 
    def dropEvent(self, event):
 
        position = event.pos()
        button = Button('Close', self)
        button.move(position)
        button.show()
 
        event.setDropAction(QtCore.Qt.MoveAction)
        event.accept()
 
 
app = QtGui.QApplication(sys.argv)
db = DragButton()
db.show()
app.exec_()

فى كود المثال، لدينا زر من الصنف QPushButton على النافذة اذا تم الضغط عليه بالزر الأيسر للفأرة سيطبع كلمة press على الكونسول. وفى حال الضغط بالزر الأيمن والتحريك سنقوم بعمل عملية سحب وإفلات على الزر.

 class Button(QtGui.QPushButton):
     def __init__(self, title, parent):
         QtGui.QPushButton.__init__(self, title, parent)

هنا انشأنا صف Button يرث QPushButton سنقوم ايضا بإعادة تعريف معالجى الأحداث mouseMoveEvent() و mousePressEvent() حيث mouseMoveEvent هى المكان الذى بدأت عنده عملية السحب والإفلات.

if event.buttons() != QtCore.Qt.RightButton: return

هنا قررنا، اننا سنقوم بعملية السحب والإفلات عن طريق الزر الأيمن للفأرة. الزر الأيسر للفأرة محجوز للضغط على الزر

 mimeData = QtCore.QMimeData()
 
 drag = QtGui.QDrag(self)
 drag.setMimeData(mimeData)
 drag.setHotSpot(event.pos() - self.rect().topLeft())

هنا ننشئ اوبجكت من الصنف QDrag

 dropAction = drag.start(QtCore.Qt.MoveAction)
 
 if dropAction == QtCore.Qt.MoveAction:
     self.close()

الطريقة start() تبدأ عملية السحب والإفلات. عند سحب الزر فإننا نقوم بحذف الودجة من المكان الأول ونعيد انشاءه بالمكان الجديد

 def mousePressEvent(self, event):
     QtGui.QPushButton.mousePressEvent(self, event)
     if event.button() == QtCore.Qt.LeftButton:
         print 'press'

عند الضغط على الزر الأيسر يتم طباعة كلمة press لاحظ استدعائنا لمعالج الحدث mousePressEvent الخاص بالأب ايضا وإلا لن نرى الزر يتم الضغط عليه.

 position = event.pos()
 button = Button('Close', self)
 button.move(position)
 button.show()

فى معالج الحدث dropEvent نكتب ماسيحدث بعد افلات الفأرة، نقل الزر واظهاره. فى مثالنا، قمنا بإعادة انشاء الزر فى الموقع الجديد عن مؤشر الفأرة

 event.setDropAction(QtCore.Qt.MoveAction)
 event.accept()

نحدد نوع الإفلات، فى حالتنا هذه هو Move

الرسم فى PyQt4

نستخدم الرسم عندما نريد تعديل او تحسين زر موجود او حتى انشاءه من الصفر. للقيام بالرسم، نستخدم واجهة التطبيق البرمجة API التى تقدمها لكن PyQt4 يتم الرسم داخل الطريقة paintEvent ويوضع كود الرسم بين الطريقتين begin()و end() وهما طريقتين خاصين بال QPainter اوبجكت

رسم نص

نبدأ برسم بعض النصوص على النافذة We begin with drawing some unicode text onto the window client area.

#!/usr/bin/python
 
# drawtext.py
 
import sys
from PyQt4 import QtGui, QtCore
 
 
class DrawText(QtGui.QWidget):
    def __init__(self, parent=None):
        QtGui.QWidget.__init__(self, parent)
 
        self.setGeometry(300, 300, 250, 150)
        self.setWindowTitle('Draw Text')
 
       self.text = u'\u041b\u0435\u0432 \u041d\u0438\u043a\u043e\u043b\u0430\
\u0435\u0432\u0438\u0447 \u0422\u043e\u043b\u0441\u0442\u043e\u0439: \n\
\u0410\u043d\u043d\u0430 \u041a\u0430\u0440\u0435\u043d\u0438\u043d\u0430'
 
 
 
    def paintEvent(self, event):
        paint = QtGui.QPainter()
        paint.begin(self)
        paint.setPen(QtGui.QColor(168, 34, 3))
        paint.setFont(QtGui.QFont('Decorative', 10))
        paint.drawText(event.rect(), QtCore.Qt.AlignCenter, self.text)
        paint.end()
 
 
app = QtGui.QApplication(sys.argv)
dt = DrawText()
dt.show()
app.exec_()

فى مثالنا نكتب بعض النصوص بazbuka. ذلك النص محاذى رأسيا وافقيا In our example, we draw some text in azbuka. The text is vertically and horizontally aligned.

def paintEvent(self, event):

الرسم يتم فى معالج الحدث paintEvent

paint = QtGui.QPainter()
paint.begin(self)
...
paint.end()

الصنف QPainter مسئول عن كل عمليات الرسم المنخفضة. كل عمليات الرسم تكون بين الطرق begin() و end()

paint.setPen(QtGui.QColor(168, 34, 3))
paint.setFont(QtGui.QFont('Decorative', 10))

هنا نحدد قلما وخطا، لنستخدمهم فى رسم النص

paint.drawText(event.rect(), QtCore.Qt.AlignCenter, self.text)

الطريقة drawText() تقوم برسم النص على النافذة

pyqt4drawtext.jpg

رسم نقاط

النقطة هى ابسط كائن رسومى، مجرد بقعة صغيرة على النافذة.

#!/usr/bin/python
 
# points.py
 
import sys, random
from PyQt4 import QtGui, QtCore
 
 
class Points(QtGui.QWidget):
    def __init__(self, parent=None):
        QtGui.QWidget.__init__(self, parent)
 
        self.setGeometry(300, 300, 250, 150)
        self.setWindowTitle('Points')
 
    def paintEvent(self, event):
        paint = QtGui.QPainter()
        paint.begin(self)
        paint.setPen(QtCore.Qt.red)
        size = self.size()
        for i in range(1000):
            x = random.randint(1, size.width()-1)
            y = random.randint(1, size.height()-1)
            paint.drawPoint(x, y)
        paint.end()
 
app = QtGui.QApplication(sys.argv)
dt = Points()
dt.show()
app.exec_()

فى مثالنا نقوم برسم 1000 نقطة حمراء على مساحة العميل

paint.setPen(QtCore.Qt.red)

نقوم بتحديد اللون للقلم وذلك بإستخدام ثابت معرف سابقا red

size = self.size()

عند كل عملية اعادة تحجيم، يتم انشاء الحدث paint نحصل على المساحة الحالية بإستخدام الطريقة size()

paint.drawPoint(x, y)

نقوم برسم النقطة بإستخدام الطريقة drawPoint()

pyqt4points.jpg

الألوان

اللون هو كائن بيعبر عن خليط من الأحمر والأخضر والأزرق RGB بقيم تتراوح بين 0 ل 255 ، نستطيع تعريف اللون بعدة طرق واشهرهاقيم RGB decimal or Hexadicemal ..نستطيع ايضا استخدام RGBA “اختصارا ل Red, Green, Blue, Alpha لنضيف بعض المعلومات الإضافية opacity, transparency

#!/usr/bin/python
 
# colors.py
 
import sys, random
from PyQt4 import QtGui, QtCore
 
 
class Colors(QtGui.QWidget):
    def __init__(self, parent=None):
        QtGui.QWidget.__init__(self, parent)
 
        self.setGeometry(300, 300, 350, 280)
        self.setWindowTitle('Colors')
 
    def paintEvent(self, event):
        paint = QtGui.QPainter()
        paint.begin(self)
 
        color = QtGui.QColor(0, 0, 0)
        color.setNamedColor('#d4d4d4')
        paint.setPen(color)
 
        paint.setBrush(QtGui.QColor(255, 0, 0, 80))
        paint.drawRect(10, 15, 90, 60)
 
        paint.setBrush(QtGui.QColor(255, 0, 0, 160))
        paint.drawRect(130, 15, 90, 60)
 
        paint.setBrush(QtGui.QColor(255, 0, 0, 255))
        paint.drawRect(250, 15, 90, 60)
 
        paint.setBrush(QtGui.QColor(10, 163, 2, 55))
        paint.drawRect(10, 105, 90, 60)
 
        paint.setBrush(QtGui.QColor(160, 100, 0, 255))
        paint.drawRect(130, 105, 90, 60)
 
        paint.setBrush(QtGui.QColor(60, 100, 60, 255))
        paint.drawRect(250, 105, 90, 60)
 
        paint.setBrush(QtGui.QColor(50, 50, 50, 255))
        paint.drawRect(10, 195, 90, 60)
 
        paint.setBrush(QtGui.QColor(50, 150, 50, 255))
        paint.drawRect(130, 195, 90, 60)
 
        paint.setBrush(QtGui.QColor(223, 135, 19, 255))
        paint.drawRect(250, 195, 90, 60)
 
        paint.end()
 
app = QtGui.QApplication(sys.argv)
dt = Colors()
dt.show()
app.exec_()

فى مثالنا قمنا بإنشاء 9 مستطيلات ملونة. الصنف الأول مكون من مستطيلات حمراء مع قيم مختلفة لل alpha

color = QtGui.QColor(0, 0, 0)
color.setNamedColor('#d4d4d4')

هنا قمنا بتعريف الرقم بإستخدام hexadecimal

paint.setBrush(QtGui.QColor(255, 0, 0, 80));
paint.drawRect(10, 15, 90, 60)

هنا قمنا بتعريف فرشاة، لرسم خلفية الشكل. الطريقة drawRect() الخاصة برسم المستطيل تأخذ 4 معاملات x, y للإحداثيات و العرض والإرتفاع للمستطيل.وذلك بإستخدام القلم والفرشاة الحاليين

pyqt4colors.jpg

الصنف QPen

الصنف QPen القلم كائن رسومى يستخدم فى رسم الخطوط، المنحنيات، محددات المستطيلات .. الخ

#!/usr/bin/python
 
# penstyles.py
 
import sys
from PyQt4 import QtGui, QtCore
 
 
class PenStyles(QtGui.QWidget):
    def __init__(self, parent=None):
        QtGui.QWidget.__init__(self, parent)
 
        self.setGeometry(300, 300, 280, 270)
        self.setWindowTitle('penstyles')
 
    def paintEvent(self, event):
        paint = QtGui.QPainter()
 
        paint.begin(self)
 
        pen = QtGui.QPen(QtCore.Qt.black, 2, QtCore.Qt.SolidLine)
 
        paint.setPen(pen)
        paint.drawLine(20, 40, 250, 40)
 
        pen.setStyle(QtCore.Qt.DashLine)
        paint.setPen(pen)
        paint.drawLine(20, 80, 250, 80)
 
        pen.setStyle(QtCore.Qt.DashDotLine)
        paint.setPen(pen)
        paint.drawLine(20, 120, 250, 120)
 
        pen.setStyle(QtCore.Qt.DotLine)
        paint.setPen(pen)
        paint.drawLine(20, 160, 250, 160)
 
        pen.setStyle(QtCore.Qt.DashDotDotLine)
        paint.setPen(pen)
        paint.drawLine(20, 200, 250, 200)
 
        pen.setStyle(QtCore.Qt.CustomDashLine)
        pen.setDashPattern([1, 4, 5, 4])
        paint.setPen(pen)
        paint.drawLine(20, 240, 250, 240)
 
        paint.end()
 
app = QtGui.QApplication(sys.argv)
dt = PenStyles()
dt.show()
app.exec_()

فى مثالنا، نقوم برسم 6 خطوط بإستخدام 6 انواع من الأقلام. هناك 5 معرفين مسبقا وواحد قمنا بتعريفه.

pen = QtGui.QPen(QtCore.Qt.black, 2, QtCore.Qt.SolidLine)

هنا قمنا بإنشاء قلم من الصنف QPen وقمنا بتحديد اللون والسمك “بالبكسلز” واستخدمنا النوع SolidLine كنوع للقلم

pen.setStyle(QtCore.Qt.CustomDashLine)
pen.setDashPattern([1, 4, 5, 4])
paint.setPen(pen)

هنا قمنا بتعريف نوع جديد من الأقلام بإستخدام setStyle وقمنا بإستدعاء الطريقة setDashPattern() واللتى تأخذ list كمعامل. يجب ان يكون هناك رقم زوجى، الأرقام الفردية بتحدد الdash والزوجية تحدد المسافة.

نمطنا هو 1بكسل داش 4بكسل مسافة 5بكسل داش 4بكسل مسافة.. الخ

pyqt4penstyles.jpg

الصنف QBrush

هى كائن رسومى، بيستخدم فى رسم خلفية الأشكال مثل المضلعات وغيرها. ممكن تكون الفرشاة من 3 انواع، نوع معرف سابقا، gradien or a texture pattern

#!/usr/bin/python
 
# brushes.py
 
import sys
from PyQt4 import QtGui, QtCore
 
 
class Brushes(QtGui.QWidget):
    def __init__(self, parent=None):
        QtGui.QWidget.__init__(self, parent)
 
        self.setGeometry(300, 300, 355, 280)
        self.setWindowTitle('Brushes')
 
    def paintEvent(self, event):
        paint = QtGui.QPainter()
 
        paint.begin(self)
 
        brush = QtGui.QBrush(QtCore.Qt.SolidPattern)
        paint.setBrush(brush)
        paint.drawRect(10, 15, 90, 60)
 
        brush.setStyle(QtCore.Qt.Dense1Pattern)
        paint.setBrush(brush)
        paint.drawRect(130, 15, 90, 60)
 
        brush.setStyle(QtCore.Qt.Dense2Pattern)
        paint.setBrush(brush)
        paint.drawRect(250, 15, 90, 60)
 
        brush.setStyle(QtCore.Qt.Dense3Pattern)
        paint.setBrush(brush)
        paint.drawRect(10, 105, 90, 60)
 
        brush.setStyle(QtCore.Qt.DiagCrossPattern)
        paint.setBrush(brush)
        paint.drawRect(10, 105, 90, 60)
 
        brush.setStyle(QtCore.Qt.Dense5Pattern)
        paint.setBrush(brush)
        paint.drawRect(130, 105, 90, 60)
 
        brush.setStyle(QtCore.Qt.Dense6Pattern)
        paint.setBrush(brush)
        paint.drawRect(250, 105, 90, 60)
 
        brush.setStyle(QtCore.Qt.Dense7Pattern)
        paint.setBrush(brush)
        paint.drawRect(250, 105, 90, 60)
 
        brush.setStyle(QtCore.Qt.HorPattern)
        paint.setBrush(brush)
        paint.drawRect(10, 195, 90, 60)
 
        brush.setStyle(QtCore.Qt.VerPattern)
        paint.setBrush(brush)
        paint.drawRect(130, 195, 90, 60)
 
        brush.setStyle(QtCore.Qt.BDiagPattern)
        paint.setBrush(brush)
        paint.drawRect(250, 195, 90, 60)
 
        paint.end()
 
app = QtGui.QApplication(sys.argv)
dt = Brushes()
dt.show()
app.exec_()

فى مثالنا نقوم برسم 6 انواع من المستطيلات.

brush = QtGui.QBrush(QtCore.Qt.SolidPattern)
paint.setBrush(brush)
paint.drawRect(10, 15, 90, 60)

قمنا بتعريف كائن من الصنف QBrush ليتم استخدامه فى عملية الرسم بإستخدام الطريقة drawRect We define a brush object. Set it to the painter object. And draw the rectangle calling the drawRect() method.

pyqt4brushes.jpg

Custom Widgets in PyQt4

هل تسألت كيف تم تنفيذ عنصر ما من الواجهة فى تطبيق ما؟ غالبا كل من يريد ان يصبح مبرمجا فعل، ثم عند النظر لقائمة الودجات اللتى تقدمها لك مكتبتك الرسومية المفضلة، ولكن لاتجدها؟ ادوات تطوير التطبيقات بتقدملك اهم الودجات مثل الأزرار، ويدجات النصوص، المنزلقات،.. الخ ولكن لايوجد من يقدم لك كل شئ.

هناك نوعين من ادوات تطوير التطبيقات، سبارطية (مثل FLTK لاتقدم لك الا الأساسيات ليقوم المبرمج بإنشاء مايريده بنفسه) وثقيلة (مثل PyQt4 مع العديد من الودجات ولكنها ايضا لاتوفر لك كل شئ مثلا ودجة لحرق اسطوانات؟ (مثل النيرو او k3B وغيرهم). وايضا ادوات فى الغالب لاتحتوى اداة تطوير التطبيقات على ال charts

لذيا يجب ان ينشئ المبرمجين هذه الودجات بأنفسهم، وذلك بإستخدام ادوات الرسم المتوفرة فى اداة التطوير.هنالك احتمالين تعديل وتحسين ودجة موجود بالفعل، او انشاء واحد من الصفر

ودجة الحرق

شبيه بما تراه فى Nero, K3B او غيرهم من برامج حرق الإسطوانات

#!/usr/bin/python
 
# burning.py
 
import sys
from PyQt4 import QtGui, QtCore
 
 
class Widget(QtGui.QLabel):
    def __init__(self, parent):
        QtGui.QLabel.__init__(self, parent)
        self.setMinimumSize(1, 30)
        self.parent = parent
        self.num = [75, 150, 225, 300, 375, 450, 525, 600, 675]
 
    def paintEvent(self, event):
        paint = QtGui.QPainter()
        paint.begin(self)
 
        font = QtGui.QFont('Serif', 7, QtGui.QFont.Light)
        paint.setFont(font)
 
        size = self.size()
        w = size.width()
        h = size.height()
        cw = self.parent.cw
        step = int(round(w / 10.0))
 
 
        till = int(((w / 750.0) * cw))
        full = int(((w / 750.0) * 700))
 
        if cw >= 700:
            paint.setPen(QtGui.QColor(255, 255, 255))
            paint.setBrush(QtGui.QColor(255, 255, 184))
            paint.drawRect(0, 0, full, h)
            paint.setPen(QtGui.QColor(255, 175, 175))
            paint.setBrush(QtGui.QColor(255, 175, 175))
            paint.drawRect(full, 0, till-full, h)
        else:
            paint.setPen(QtGui.QColor(255, 255, 255))
            paint.setBrush(QtGui.QColor(255, 255, 184))
            paint.drawRect(0, 0, till, h)
 
 
        pen = QtGui.QPen(QtGui.QColor(20, 20, 20), 1, QtCore.Qt.SolidLine)
        paint.setPen(pen)
        paint.setBrush(QtCore.Qt.NoBrush)
        paint.drawRect(0, 0, w-1, h-1)
 
	j = 0
 
        for i in range(step, 10*step, step):
            paint.drawLine(i, 0, i, 5)
            metrics = paint.fontMetrics()
            fw = metrics.width(str(self.num[j]))
            paint.drawText(i-fw/2, h/2, str(self.num[j]))
            j = j + 1
 
        paint.end()
 
class Burning(QtGui.QWidget):
    def __init__(self, parent=None):
        QtGui.QWidget.__init__(self, parent)
 
        self.cw = 75
 
        self.slider = QtGui.QSlider(QtCore.Qt.Horizontal, self)
        self.slider.setFocusPolicy(QtCore.Qt.NoFocus)
        self.slider.setRange(1, 750)
        self.slider.setValue(75)
        self.slider.setGeometry(30, 40, 150, 30)
 
        self.wid = Widget(self)
 
        self.connect(self.slider, QtCore.SIGNAL('valueChanged(int)'), self.changeValue)
        hbox = QtGui.QHBoxLayout()
        hbox.addWidget(self.wid)
        vbox = QtGui.QVBoxLayout()
        vbox.addStretch(1)
        vbox.addLayout(hbox)
        self.setLayout(vbox)
 
        self.setGeometry(300, 300, 300, 220)
        self.setWindowTitle('Burning')
 
    def changeValue(self, event):
        self.cw = self.slider.value()
        self.wid.repaint()
 
 
app = QtGui.QApplication(sys.argv)
dt = Burning()
dt.show()
app.exec_()

فى مثالنا، لدينا منزلق و ودجة قمنا بإنشاءه خصيصا ويتم التحكم فيه عن طريق المنزلق.هذا الودجة يعرض السعة لوسيط تخزين والمساحة الفارغة فيه لنا. اقل قيمة مقبولة هى 1 واكبر قيمة هى 750 وحينما يتم تعدى القيمة 700 نقوم بوضع الرسم بلون احمر “ليعبر عن الحرق الزائد”

ودجة الحرق موضوع بأسفل النافذة وذلك عن طريق مخطط افقى QHBoxLayout و QVBoxLayout

 class Widget(QtGui.QLabel):
     def __init__(self, parent):
         QtGui.QLabel.__init__(self, parent)

ودجة الحرق يرث ال QLabel

self.setMinimumSize(1, 30)

نقوم بتعديل اقل مساحة (ارتفاع) الودجة لأن القيمة الأساسية صغيرة بالنسبة لنا

font = QtGui.QFont('Serif', 7, QtGui.QFont.Light)
paint.setFont(font)

نستخدم خط اصغر من الإفتراضى ليناسبنا اكثر.

 size = self.size()
 w = size.width()
 h = size.height()
 cw = self.parent.cw
 step = int(round(w / 10.0))
 
 till = int(((w / 750.0) * cw))
 full = int(((w / 750.0) * 700))

نرسم الودجة بصورة ديناميكية، الأكبر النافذة، الأكبر ودجة الحرق والعكس. لذا نحسب المساحة للودجة، المعامل till بيحدد المساحة الكلية للرسم، هذه القيمة تأتى من المنزلق وهى النسبة من المساحة الكلية. المعامل full يحدد النقطة اللتى سنبدأ عندها برسم اللون الأحمر “حرق زائد”. لاحظ استخدام الأعداد الحقيقة لضمان اكبر دقة.

عملية الرسم تضمن 3 خطوات، رسم الأصفر او الأحمر، والمستطيل الأصفر ثم رسم الخطوط الرأسية لتقسم الودجة لعدة اقسام، واخيرا نقوم برسم الأرقام لتعبر عن سعة وسيط التخزين.

 metrics = paint.fontMetrics()
 fw = metrics.width(str(self.num[j]))
 paint.drawText(i-fw/2, h/2, str(self.num[j]))

نستخدم ال font metrics لرسم النص. يجب ان نعلم عرض النص لنقوم بوضعه فى المنتصف حول الخط الأرسى

pyqt4burning.jpg

لعبة Tetris بإستخدام PyQt4

عمل لعبة كمبيوتر شئ غير سهل، وعاجلا او اجلا سيريد المبرمج عمل لعبة، فى الواقع العديد من الناس اصبحو مهتمين بالبرمجة رغبتهم فى انشاء العاب خاصة بهم. على كل حال، كتابة لعبة سيفيدك فى تحسين مهاراتك البرمجية

Tetris

واحدة من اشهر الألعاب على الإطلاق. اللعبة الأصلية صممت وبرمجت بواسطة Alexey Pajitnov فى عام 1985، ومنذ ذلك الحين اصبحت موجودة تقريبا على معظم الأجهزة، حتى الخلوى

تسمى ايضا احجية القطع المتساقطة. فى هذه اللعبة لدينا 7 انواع من القطع مسماه tetominoes وهى S-shape, Z-shape, T-shape, L-shape, Line-shape, MirroredL-shape and a Square-shape.

كل من هذه الأشكال مكون من 4 مربعات. وتتساقط القطع على لوحة. ممكن ازاحة او تدوير القطع ليجعلهم مناسبين بأقصى قدر. اذا كونا صف فيتم حذفه وإضافته للنتيجة النهائية، تظل اللعبة حتى يصل عدد الصفوف للأعلى

اداة التطوير PyQt4 مخصصة لتطوير التطبيقات مع انه يمكن تطوير العاب بها كما سنرى، فى حين هناك بعض المكتبات موجهة لتطوير العاب الحاسوب.

المثال التالى هو اصدار معدل من لعبة tetris المتاحة مع ملفات تنصيب PyQt4

التطوير

ليس لدينا صور للعبة، لذا سنقوم برسمها بإستخدام ال APIs الخاصة بالرسم فى PyQt4 ، اى لعبة عبارة عن تمثيل رياضى وكذلك tetris

بعض الأفكار وراء اللعبة

*استخدام QtCore.QBasicTimer() لإدارة اللعبة *رسم القطع *القطع تتحرك بالمربع وليس بالبيكسل *رياضيا، اللوحة ماهى الا مجموعة بسيطة من الأرقام Some ideas behind the game.

#!/usr/bin/python
 
# tetris.py
 
import sys
import random
from PyQt4 import QtCore, QtGui
 
 
class Tetris(QtGui.QMainWindow):
    def __init__(self):
        QtGui.QMainWindow.__init__(self)
 
        self.setGeometry(300, 300, 180, 380)
        self.setWindowTitle('Tetris')
	self.tetrisboard = Board(self)
 
	self.setCentralWidget(self.tetrisboard)
 
	self.statusbar = self.statusBar()
	self.connect(self.tetrisboard, QtCore.SIGNAL("messageToStatusbar(QString)"), 
	    self.statusbar, QtCore.SLOT("showMessage(QString)"))
 
	self.tetrisboard.start()
        self.center()
 
    def center(self):
        screen = QtGui.QDesktopWidget().screenGeometry()
        size =  self.geometry()
        self.move((screen.width()-size.width())/2, 
	    (screen.height()-size.height())/2)
 
class Board(QtGui.QFrame):
    BoardWidth = 10
    BoardHeight = 22
    Speed = 300
 
    def __init__(self, parent):
        QtGui.QFrame.__init__(self, parent)
 
        self.timer = QtCore.QBasicTimer()
        self.isWaitingAfterLine = False
        self.curPiece = Shape()
        self.nextPiece = Shape()
        self.curX = 0
        self.curY = 0
        self.numLinesRemoved = 0
        self.board = []
 
        self.setFocusPolicy(QtCore.Qt.StrongFocus)
        self.isStarted = False
        self.isPaused = False
        self.clearBoard()
 
        self.nextPiece.setRandomShape()
 
    def shapeAt(self, x, y):
        return self.board[(y * Board.BoardWidth) + x]
 
    def setShapeAt(self, x, y, shape):
        self.board[(y * Board.BoardWidth) + x] = shape
 
    def squareWidth(self):
        return self.contentsRect().width() / Board.BoardWidth
 
    def squareHeight(self):
        return self.contentsRect().height() / Board.BoardHeight
 
    def start(self):
        if self.isPaused:
            return
 
        self.isStarted = True
        self.isWaitingAfterLine = False
        self.numLinesRemoved = 0
        self.clearBoard()
 
        self.emit(QtCore.SIGNAL("messageToStatusbar(QString)"), 
	    str(self.numLinesRemoved))
 
        self.newPiece()
        self.timer.start(Board.Speed, self)
 
    def pause(self):
        if not self.isStarted:
            return
 
        self.isPaused = not self.isPaused
        if self.isPaused:
            self.timer.stop()
            self.emit(QtCore.SIGNAL("messageToStatusbar(QString)"), "paused")
        else:
            self.timer.start(Board.Speed, self)
            self.emit(QtCore.SIGNAL("messageToStatusbar(QString)"), 
	        str(self.numLinesRemoved))
 
        self.update()
 
    def paintEvent(self, event):
        painter = QtGui.QPainter(self)
        rect = self.contentsRect()
 
        boardTop = rect.bottom() - Board.BoardHeight * self.squareHeight()
 
        for i in range(Board.BoardHeight):
            for j in range(Board.BoardWidth):
                shape = self.shapeAt(j, Board.BoardHeight - i - 1)
                if shape != Tetrominoes.NoShape:
                    self.drawSquare(painter,
                        rect.left() + j * self.squareWidth(),
                        boardTop + i * self.squareHeight(), shape)
 
        if self.curPiece.shape() != Tetrominoes.NoShape:
            for i in range(4):
                x = self.curX + self.curPiece.x(i)
                y = self.curY - self.curPiece.y(i)
                self.drawSquare(painter, rect.left() + x * self.squareWidth(),
                    boardTop + (Board.BoardHeight - y - 1) * self.squareHeight(),
                    self.curPiece.shape())
 
    def keyPressEvent(self, event):
        if not self.isStarted or self.curPiece.shape() == Tetrominoes.NoShape:
            QtGui.QWidget.keyPressEvent(self, event)
            return
 
        key = event.key()
	if key == QtCore.Qt.Key_P:
	    self.pause()
            return
	if self.isPaused:
            return
        elif key == QtCore.Qt.Key_Left:
            self.tryMove(self.curPiece, self.curX - 1, self.curY)
        elif key == QtCore.Qt.Key_Right:
            self.tryMove(self.curPiece, self.curX + 1, self.curY)
        elif key == QtCore.Qt.Key_Down:
            self.tryMove(self.curPiece.rotatedRight(), self.curX, self.curY)
        elif key == QtCore.Qt.Key_Up:
            self.tryMove(self.curPiece.rotatedLeft(), self.curX, self.curY)
        elif key == QtCore.Qt.Key_Space:
            self.dropDown()
        elif key == QtCore.Qt.Key_D:
            self.oneLineDown()
        else:
            QtGui.QWidget.keyPressEvent(self, event)
 
    def timerEvent(self, event):
        if event.timerId() == self.timer.timerId():
            if self.isWaitingAfterLine:
                self.isWaitingAfterLine = False
                self.newPiece()
            else:
                self.oneLineDown()
        else:
            QtGui.QFrame.timerEvent(self, event)
 
    def clearBoard(self):
        for i in range(Board.BoardHeight * Board.BoardWidth):
	    self.board.append(Tetrominoes.NoShape)
 
    def dropDown(self):
        newY = self.curY
        while newY > 0:
            if not self.tryMove(self.curPiece, self.curX, newY - 1):
                break
            newY -= 1
 
        self.pieceDropped()
 
    def oneLineDown(self):
        if not self.tryMove(self.curPiece, self.curX, self.curY - 1):
            self.pieceDropped()
 
    def pieceDropped(self):
        for i in range(4):
            x = self.curX + self.curPiece.x(i)
            y = self.curY - self.curPiece.y(i)
            self.setShapeAt(x, y, self.curPiece.shape())
 
        self.removeFullLines()
 
        if not self.isWaitingAfterLine:
            self.newPiece()
 
    def removeFullLines(self):
        numFullLines = 0
 
	rowsToRemove = []
 
	for i in range(Board.BoardHeight):
	    n = 0
            for j in range(Board.BoardWidth):
                if not self.shapeAt(j, i) == Tetrominoes.NoShape:
                    n = n + 1
 
	    if n == 10:
		rowsToRemove.append(i)
 
	rowsToRemove.reverse()
 
	for m in rowsToRemove:
	    for k in range(m, Board.BoardHeight):
	        for l in range(Board.BoardWidth):
                    self.setShapeAt(l, k, self.shapeAt(l, k + 1))
 
        numFullLines = numFullLines + len(rowsToRemove)
 
        if numFullLines > 0:
            self.numLinesRemoved = self.numLinesRemoved + numFullLines
            self.emit(QtCore.SIGNAL("messageToStatusbar(QString)"), 
		str(self.numLinesRemoved))
            self.isWaitingAfterLine = True
            self.curPiece.setShape(Tetrominoes.NoShape)
            self.update()
 
    def newPiece(self):
        self.curPiece = self.nextPiece
        self.nextPiece.setRandomShape()
        self.curX = Board.BoardWidth / 2 + 1
        self.curY = Board.BoardHeight - 1 + self.curPiece.minY()
 
        if not self.tryMove(self.curPiece, self.curX, self.curY):
            self.curPiece.setShape(Tetrominoes.NoShape)
            self.timer.stop()
            self.isStarted = False
            self.emit(QtCore.SIGNAL("messageToStatusbar(QString)"), "Game over")
 
 
 
    def tryMove(self, newPiece, newX, newY):
        for i in range(4):
            x = newX + newPiece.x(i)
            y = newY - newPiece.y(i)
            if x < 0 or x >= Board.BoardWidth or y < 0 or y >= Board.BoardHeight:
                return False
            if self.shapeAt(x, y) != Tetrominoes.NoShape:
                return False
 
        self.curPiece = newPiece
        self.curX = newX
        self.curY = newY
        self.update()
        return True
 
    def drawSquare(self, painter, x, y, shape):
        colorTable = [0x000000, 0xCC6666, 0x66CC66, 0x6666CC,
                      0xCCCC66, 0xCC66CC, 0x66CCCC, 0xDAAA00]
 
        color = QtGui.QColor(colorTable[shape])
        painter.fillRect(x + 1, y + 1, self.squareWidth() - 2, 
	    self.squareHeight() - 2, color)
 
        painter.setPen(color.light())
        painter.drawLine(x, y + self.squareHeight() - 1, x, y)
        painter.drawLine(x, y, x + self.squareWidth() - 1, y)
 
        painter.setPen(color.dark())
        painter.drawLine(x + 1, y + self.squareHeight() - 1,
            x + self.squareWidth() - 1, y + self.squareHeight() - 1)
        painter.drawLine(x + self.squareWidth() - 1, 
	    y + self.squareHeight() - 1, x + self.squareWidth() - 1, y + 1)
 
 
class Tetrominoes(object):
    NoShape = 0
    ZShape = 1
    SShape = 2
    LineShape = 3
    TShape = 4
    SquareShape = 5
    LShape = 6
    MirroredLShape = 7
 
 
class Shape(object):
    coordsTable = (
        ((0, 0),     (0, 0),     (0, 0),     (0, 0)),
        ((0, -1),    (0, 0),     (-1, 0),    (-1, 1)),
        ((0, -1),    (0, 0),     (1, 0),     (1, 1)),
        ((0, -1),    (0, 0),     (0, 1),     (0, 2)),
        ((-1, 0),    (0, 0),     (1, 0),     (0, 1)),
        ((0, 0),     (1, 0),     (0, 1),     (1, 1)),
        ((-1, -1),   (0, -1),    (0, 0),     (0, 1)),
        ((1, -1),    (0, -1),    (0, 0),     (0, 1))
    )
 
    def __init__(self):
        self.coords = [[0,0] for i in range(4)]
        self.pieceShape = Tetrominoes.NoShape
 
        self.setShape(Tetrominoes.NoShape)
 
    def shape(self):
        return self.pieceShape
 
    def setShape(self, shape):
        table = Shape.coordsTable[shape]
        for i in range(4):
            for j in range(2):
                self.coords[i][j] = table[i][j]
 
        self.pieceShape = shape
 
    def setRandomShape(self):
        self.setShape(random.randint(1, 7))
 
    def x(self, index):
        return self.coords[index][0]
 
    def y(self, index):
        return self.coords[index][1]
 
    def setX(self, index, x):
        self.coords[index][0] = x
 
    def setY(self, index, y):
        self.coords[index][1] = y
 
    def minX(self):
        m = self.coords[0][0]
        for i in range(4):
            m = min(m, self.coords[i][0])
 
        return m
 
    def maxX(self):
        m = self.coords[0][0]
        for i in range(4):
            m = max(m, self.coords[i][0])
 
        return m
 
    def minY(self):
        m = self.coords[0][1]
        for i in range(4):
            m = min(m, self.coords[i][1])
 
        return m
 
    def maxY(self):
        m = self.coords[0][1]
        for i in range(4):
            m = max(m, self.coords[i][1])
 
        return m
 
    def rotatedLeft(self):
        if self.pieceShape == Tetrominoes.SquareShape:
            return self
 
        result = Shape()
        result.pieceShape = self.pieceShape
        for i in range(4):
            result.setX(i, self.y(i))
            result.setY(i, -self.x(i))
 
        return result
 
    def rotatedRight(self):
        if self.pieceShape == Tetrominoes.SquareShape:
            return self
 
        result = Shape()
        result.pieceShape = self.pieceShape
        for i in range(4):
            result.setX(i, -self.y(i))
            result.setY(i, self.x(i))
 
        return result
 
 
app = QtGui.QApplication(sys.argv)
tetris = Tetris()
tetris.show()
sys.exit(app.exec_())

قمت بتبسيط اللعبة قليلا لتصبح اسهل للفهم. تبدأ اللعبة مباشرة بعد تشغيلها، نستطيع ايقافها بالضغط على الزر p وزر المسافة سيقوم بإنزال القطعة فورا الى الأسفل. السرعة ثابتة فى اللعبة، لم يتم اضافة اى تسريع. يتم حساب النتيجة بعدد الأسطر اللتى تم حذفها

 self.statusbar = self.statusBar()
 self.connect(self.tetrisboard, QtCore.SIGNAL("messageToStatusbar(QString)"), 
     self.statusbar, QtCore.SLOT("showMessage(QString)"))

بننشئ شريط حالة، لنعرض الرسائل(عدد الأسطر المنتهية، اللعبة متوقفة، انتهاء اللعبة)

 ...
 self.curX = 0
 self.curY = 0
 self.numLinesRemoved = 0
 self.board = []
 ...

قبل ان نبدأ اللعبة، يجب ان نجهز بعض المتغيرات الهامة. مثل اللوحة وهى عبارة عن مجموعة الأرقام من 0 الى 7 تمثل المركز للقطع والبقايا. وعدد الأسطر/الصفوف المحذوفه.. الخ

 for j in range(Board.BoardWidth):
     shape = self.shapeAt(j, Board.BoardHeight - i - 1)
     if shape != Tetrominoes.NoShape:
         self.drawSquare(painter,
             rect.left() + j * self.squareWidth(),
             boardTop + i * self.squareHeight(), shape)

عملية الرسم مقسمه لخطوتين: الخطوة الأولى: رسم كل القطع او البقايا اللتى سقطت بأسفل اللوحة، كل المربعات يتم تذكرها فى اللوحة self.board ونصل إليها بإستخدام الطريقة shapeAt()

 if self.curPiece.shape() != Tetrominoes.NoShape:
     for i in range(4):
         x = self.curX + self.curPiece.x(i)
         y = self.curY - self.curPiece.y(i)
         self.drawSquare(painter, rect.left() + x * self.squareWidth(),
             boardTop + (Board.BoardHeight - y - 1) * self.squareHeight(),
             self.curPiece.shape())

الخطوة الثانية: رسم القطعة الساقطة The next step is drawing of the actual piece, that is falling down.

 elif key == QtCore.Qt.Key_Left:
     self.tryMove(self.curPiece, self.curX - 1, self.curY)
 elif key == QtCore.Qt.Key_Right:
     self.tryMove(self.curPiece, self.curX + 1, self.curY)

فى معالج الحدث keyPressEvent نختبر الأزار اللتى يتم الضغط عليها، اذا تم ضغط السهم الأيمن فإننا نحاول تحريك القطعة الى اليمين(نحاول لأن قد لانستطيع تحريكها!)

 def tryMove(self, newPiece, newX, newY):
     for i in range(4):
         x = newX + newPiece.x(i)
         y = newY - newPiece.y(i)
         if x < 0 or x >= Board.BoardWidth or y < 0 or y >= Board.BoardHeight:
             return False
         if self.shapeAt(x, y) != Tetrominoes.NoShape:
             return False
 
     self.curPiece = newPiece
     self.curX = newX
     self.curY = newY
     self.update()
     return True

فى الطريقة tryMove() نحاول تحريك القطع، فى حال وجود القطعة عند الحافة او المجاور فإننا نعيد قيمة False وإلا نقوم بوضع القطعة فى الموقع الجديد

 def timerEvent(self, event):
     if event.timerId() == self.timer.timerId():
         if self.isWaitingAfterLine:
             self.isWaitingAfterLine = False
             self.newPiece()
         else:
             self.oneLineDown()
     else:
         QtGui.QFrame.timerEvent(self, event)

فى معالج الحدث timerEvent اما ننشئ قطعة جديدة تلى اللتى تم اسقاطها، او تحريك تلك القطع للأسفل

 def removeFullLines(self):
     numFullLines = 0
 
     rowsToRemove = []
 
     for i in range(Board.BoardHeight):
         n = 0
         for j in range(Board.BoardWidth):
             if not self.shapeAt(j, i) == Tetrominoes.NoShape:
                 n = n + 1
 
         if n == 10:
             rowsToRemove.append(i)
 
      rowsToRemove.reverse()
 
      for m in rowsToRemove:
          for k in range(m, Board.BoardHeight):
              for l in range(Board.BoardWidth):
                  self.setShapeAt(l, k, self.shapeAt(l, k + 1))
 ...

عندما تصطدم القطعة بالأسفل نستدعى الطريقة removeFullLines() لحذف الصفوف المكتملة، ويتم ذلك بنقل كل الصفوف فوق الصف المكتمل. لاحظ عكس ترتيب الأسطر المحذوفة وإلا لن تعمل بصورة سليمة. فى حالتنا هذه نستخدم naive gravity مما يعنى ان القطع ربما تطفو فوق فراغات.

 def newPiece(self):
     self.curPiece = self.nextPiece
     self.nextPiece.setRandomShape()
     self.curX = Board.BoardWidth / 2 + 1
     self.curY = Board.BoardHeight - 1 + self.curPiece.minY()
 
     if not self.tryMove(self.curPiece, self.curX, self.curY):
         self.curPiece.setShape(Tetrominoes.NoShape)
         self.timer.stop()
         self.isStarted = False
         self.emit(QtCore.SIGNAL("messageToStatusbar(QString)"), "Game over")

الطريقة newPiece() تنشئ قطعة عشوائية. اذا لم تستطع هذه القطعة التحرك فإن اللعبة انتهت.

الصف Shape يقوم بتخزين المعلومات عن القطعة

 self.coords = [[0,0] for i in range(4)]

عند الإنشاء قمنا بعمل قائمة من الأحداثيات لتخزين احداثيات شكل القطعة.

عندما نرسم القطعة الساقطة نرسمها عند ال self.curX, self.curY وبعد ذلك ننظر الى جدول الأحداثيات لنرسم ال 4 مربعات.

1) الترجمة الشائعة لكلمة class هي صف كما في مصطلحات نظرية التطور في الأحياء لكنه تعريب غير موفق لأن السياق هنا يحتوي على صفوف rows
2) الودجة هي الكائن الرسومي لأنها وسيلة وجاء في لسان العرب “ووَدَجَ بين القوم وَدْجاً أَصلح وفلانٌ وَدَجِي إِلى فلان أَي وسيلتي” انظر http://wiki.arabeyes.org/techdict_talk:Widget
3) التلميحات كانت تسمى بالونات المساعدة في نظام ماكنتوش
4) لا أنصح بتغير الخط التلقائي فمكان ذلك في إعدادات النظام والتطبيق يجب أن ينصاع لتفضيلات المستخدم –السعدي
5) يمكنك الرجوع لصفحة صنوف PyQT
6) يفترض بالتطبيقات عدم استخدم الاحداثيات في توضيب أماكن الودجات بقيم ثابتة لأن الأماكن يجب أن تعكس في بعض اللغات مثل العربية كما أن اختلاف الخطوط قد يجعل النص يفيض أو يقص إن كان الخط كبيرا لذا يجب استخدام أساليب متقدمة في الرصف انظر إدارة المخطط –السعدي
7) التَّلَمُ مشَقُّ الكِراب في الأَرض بلغة أَهل اليمن وأَهل الغَوْر وقيل كل أُخْدُودٍ من أَخاديد الأَرض والجمع أَتْلامٌ وهو التِّلامُ والجمع تُلُم وقيل التِّلامُ أَثَرُ اللُّومَةِ في الأَرض وجمعها التُّلُم –لسان العرب
8) كان بإمكانه استعمال pass عوضا عن عمل كائن QLabel
آخر تعديل:: 23 نيسان 2015 الساعة 00:20 (تحرير خارجي)