اذهب إلى المحتوى

البرمجة الحدثية Event Driven Programming المساقة بالأحداث


أسامة دمراني

درسنا حتى الآن البرامج جزئية التوجه batch oriented programs، والتي تستدعى بإحدى طريقتين: جزئية التوجه batch oriented، حيث تبدأ البرامج بالعمل ثم تنفذ شيئًا ما ثم تتوقف، أو مدفوعة بالأحداث أو حدثية التوجه event driven، أي تبدأ البرامج وتنتظر وقوع أحداث بعينها؛ ولا تتوقف إلا حين يأمرها حدث آخر بالتوقف.

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

وسنغطي في هذا المقال النقاط التالية:

  • أوجه اختلاف البرامج المدفوعة بالأحداث عن البرامج جزئية التوجه.
  • كيفية كتابة حلقة حدث تكرارية.
  • كيفية استخدام إطار عمل أحداث مثل Tkinter.

محاكاة حلقة أحداث تكرارية

يحتوي البرنامج المدفوع بالأحداث أو البرنامج الحدثي event driven program على حلقة تلتقط الأحداث المستَلمة وتعالجها، وقد تولد بيئة التشغيل الأحداث -كما يحدث في جميع البرامج الرسومية تقريبًا-، أو يبحث البرنامج عن الأحداث -كما يحدث في أنظمة التحكم المدمجة التي في الكاميرات وغيرها-، وسنكتب برنامجًا يبحث عن نوع واحد من الأحداث، وهو مدخلات لوحة المفاتيح، ويعالج النتائج إلى أن يستلم حدث خروج quit، والذي سيكون حالتنا مفتاح المسافة space على لوحة المفاتيح، وسنعالج الأحداث المدخلة بطريقة سهلة، إذ سنطبع ترميز آسكي ASCII الخاص بالمفتاح الذي ضغطه المستخدم، وسنستخدم بايثون لأنها تحوي دالة getch()‎ سهلة الاستخدام، وسنقرأ المفاتيح مفتاحًا تلو الآخر، وتأتي هذه الدالة في صورتين وفقًا لنظام التشغيل الذي نستخدمه، فنجدها في لينكس في وحدة curses، أما في ويندوز فستكون في وحدة msvcrt، وسنستخدم نسخة ويندوز أولًا ثم نناقش خيار لينكس بالتفصيل، ويجب أن نشغل هذه البرامج من سطر أوامر النظام، لأن بيئات التطوير -مثل IDLE- ستلتقط ضربات لوحة المفاتيح التقاطًا مختلفًا.

وسنطبق دوال معالجة الأحداث أولًا، والتي تُستدعى عند التقاط ضغطة أحد المفاتيح، ثم متن البرنامج الرئيسي الذي يبدأ حلقة جمع الأحداث؛ ويستدعي دالة معالجة الحدث المناسبة عند التقاط حدث صالح.

تطبيق المثال في ويندوز

إليك التطبيق التالي:

import msvcrt
import sys

# معالجات الأحداث أولًا
def doKeyEvent(key):
    if key == '\x00' or key == '\xe0':
       key = msvcrt.getch()
    print ( ord(key), ' ', end='')
    sys.stdout.flush() # make sure it appears on screen

def doQuit(key):
    print() # أدخل سطرًا جديدًا
    raise SystemExit

# أخل مساحة على الشاشة أولًا
lines = 25 
for n in range(lines): print()

# والآن حلقة الحدث الأساسية
while True:
    ky = msvcrt.getch()
    if len(str(ky)) != 0:
        # we have a real event
        if " " in str(ky):
            doQuit(ky)
        else: 
            doKeyEvent(ky)

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

كما نلاحظ أن getch()‎ تعيد بايتات، لذا نحتاج إلى تحويلها إلى سلسلة نصية، وإلى استخدام اختبار in للتحقق متى يمكن الخروج بما أن السلسلة الناتجة لن تكون حرفًا.

وفي الحالة التي لا يكون فيها المفتاح محرف آسكي، كأن يكون مفتاحًا وظيفيًا مثلًا، وهي المفاتيح التي تحمل حرف F في أولها أعلى لوحة المفاتيح، فسنحتاج إلى جلب محرف ثانٍ من لوحة المفاتيح، لأن هذه المفاتيح الخاصة تولد أزواجًا من البايتات، أما getch()‎ فلا تجلب إلى بايتًا واحدًا في كل مرة، وتكون القيمة المهمة التي نريدها هي البايت الثاني فعليًا.

تطبيق المثال في لينكس وماك

لا يستطيع المبرمجون الذين يستخدمون أنظمة تشغيل لينكس وماك استخدام مكتبة msvcrt، لذا يستخدمون وحدةً أخرى تسمى curses، وتكون الشيفرة الناتجة شبيهةً بشيفرة ويندوز، مع بعض التعديلات التي يجب إجراؤها، كما يلي:

import curses as c

def doKeyEvent(key):
    if key == '\x00' or key == '\xe0': # non ASCII key
       key = screen.getch()     # fetch second character
    screen.addstr(str(key)+' ') # uses global screen variable

def doQuitEvent(key):
    c.resetty() # set terminal settings
    c.endwin()  # end curses session
    raise SystemExit


# امسح الشاشة واحفظ الإعدادات الحالية
# وأوقف الطباعة الآلية للمحارف على الشاشة
# ثم أخبر المستخدم ما يفعله للخروج

screen = c.initscr()
c.savetty()
c.noecho()
screen.addstr("Hit space to end...\n")

# والآن تعمل الحلقة الأساسية بلا نهاية
while True:
     ky = screen.getch()
     if ky != -1:
       # send events to event handling functions
       if ky == ord(" "): # check for quit event
         doQuitEvent(ky)
       else: 
         doKeyEvent(ky)

c.endwin()

لا تعمل أوامر الطباعة العادية في وحدة curses، بل يجب أن نستخدم دوال معالجة الشاشة الخاصة بوحدة curses، كما تعيد getch هنا ‎-1 إذا لم يُضغط على أي مفتاح -بدلًا من سلسلة فارغة-، ويطابق منطق البرنامج نسخة ويندوز السابقة فيما عدا ذلك.

ويجب أن تستعيد curses.endwin()‎ شاشتنا إلى الحالة العادية، لكنها قد لا تعمل أحيانًا، فإذا اختفى المؤشر أو لم نحصل على محرف إرجاع لبداية السطر أو غير ذلك، فسنصلح المشكلة بالخروج من بايثون باستخدام Ctrl+D واستخدام الأمر التالي:

$ stty echo -nl

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

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

برنامج رسومي

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

وفي مثالنا هنا ننشئ صنف تطبيق application class اسمه KeysApp ينشئ الواجهة الرسومية في التابع __init__، ويربط مفتاح المسافة بالتابع doQuitEvent، كما يعرّف الصنف تابع doQuitEvent المطلوب، أما الواجهة الرسومية نفسها فتتكون ببساطة من ودجِت widget (تطبيق مُصغَّر) لإدخال النصوص؛ سلوكها الافتراضي هو طباعة المحارف المدخَلة على الشاشة.

إن إنشاء صنف تطبيق في البيئات الحدثية كائنية التوجه أمر شائع، بسبب الارتباط بين مفاهيم الأحداث المرسَلة إلى برنامج ما والرسائل المرسَلة إلى كائن، إذ يرتبطان ببعضهما بسهولة كبيرة، وبذلك تصبح دالة معالجة الحدث تابعًا لصنف التطبيق، وبعد أن عرّفنا الصنف سننشئ نسخةً منه ونرسل إليها رسالة mainloop، وستبدو الشيفرة كما يلي: ً

# للحفظ، بما أن علينا تقديم كل شيءfrom X import * استخدم
# tkinter.xxx كـ
from tkinter import *
import sys


# أنشئ صنف التطبيق الذي يعرف الواجهة الرسومية وتوابع
# معالجة الأحداث
class KeysApp(Frame):
    def __init__(self): # use constructor to build GUI
        super().__init__()
        self.txtBox = Text(self)
        self.txtBox.bind("<space>", self.doQuitEvent)
        self.txtBox.pack()
        self.pack()

    def doQuitEvent(self,event):
        sys.exit()


# والآن أنشئ نسخة وابدأ تشغيل حلقة الحدث
myApp = KeysApp()
myApp.mainloop()

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

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

self.txtBox.bind("<Key>", self.doKeyEvent)

كما سنضيف التابع التالي لمعالجة الحدث:

def doKeyEvent(self,event):
    str = "%d\n" % event.keycode
    self.txtBox.insert(END, str)
    return "break"

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

كما نلاحظ أن return "break"‎ هي إشارة سحرية تخبر Tkinter بأن لا يستدعي معالجة الأحداث الافتراضية لهذه الودجِت، وبدون هذا السطر سيعرض الصندوق النصي رمز آسكي متبوعًا بالمحرف الحقيقي الذي ضُغط، وليس هذا ما نريده.

إلى هنا يكفي الحديث عن Tkinter، فلم نكن ننو شرحه هنا وإنما في مقال تالٍ.

البرمجة الحدثية في VBScript وجافاسكربت

نستطيع تطبيق البرمجة الحدثية في كل من جافاسكربت وVBScript عند برمجة متصفح، فعادةً إذا حُمِّلَت صفحة ويب تحتوي على سكربت فإن السكربت ينفَّذ جزءًا جزءًا مع تحميل الصفحة، لكن إذا لم يحوِ السكربت إلا تعريفات الدوال فلن يفعل التنفيذ شيئًا إلا تعريف الدوال على أنها جاهزة للاستخدام، أما الدوال نفسها فلن تُستدعى هنا ابتداءً، بل ستكون مقيدةً إلى عناصر HTML في الجزء الخاص بشيفرة HTML في الصفحة -داخل عنصر Form غالبًا-، بحيث تُستدعى هذه الدوال عند وقوع الأحداث، وقد رأينا هذا في مثال جافاسكربت الخاص بالحصول على مدخلات المستخدم عندما نقرأ المدخلات من استمارة HTML، لننظر في هذا المثال مرةً أخرى؛ ونر كيف أنه تطبيق عملي على برمجة حدَثية في صفحة ويب:

<form name='entry'>
<p>Type value then click outside the field with your mouse</p>
<input Type='text' 
          Name='data' 
          onChange='alert("We got a value of " + document.entry.data.value);'/>
</form>

نلاحظ عدم وجود تعريف لدالة جافاسكربت، بل مجرد استدعاء لـ alert المرتبطة بسمة onChange لعنصر input، وهذه السمة هي إحدى الأحداث التي تستطيع عناصر HTML توليدها، ونستطيع ربط أي شيفرة جافاسكربت عشوائية لتنفيذها في كل مرة تقع فيها هذه الأحداث، كما يمكن إنشاء دالة واستدعاؤها بدلًا من استدعاء alert كما يلي:

<script type="text/javascript">
function echoValue(){
   alert("We got a value of " + document.entry.data.value);
}
</script>

<form name='entry'>
<p>Type value then click outside the field with your mouse</p>
<input Type='text' Name='data' onChange='echoValue()'/>
</form>

يعرّف الجزء الخاص بالسكربت دالة جافاسكربت هي echoValue تحاكي استدعاء alert الذي كان معنا من قبل، ويحتوي عنصر input الآن على دالة مسندة على أنها معالج الأحداث للسمة onChange، ثم تنفَّذ الدالة عند تغير قيمة الدخل، وتكون حلقة الحدث التي تلتقط الأحداث مضمنةً داخل المتصفح.

ورغم أن دالتنا هذه استدعت alert إلا أنها تستطيع أكثر من ذلك، بل من الممكن جعلها برنامجًا معقدًا بذاتها، وذلك وفقًا لما نضعه في متنها.

يمكن استخدام VBScript بنفس الطريقة، عدا أن تعريفات الدوال ستكون بـ VBscript بدلًا من جافاسكربت، كما يلي:

<script type="text/vbscript">
Sub EchoInput()
   MsgBox "We got a value of " & Document.entry2.data.value
End Sub
</script>

<form name='entry2'>
<p>Type value then click outside the field with your mouse</p>
<input Type='text' Name='data' onChange='EchoInput()'/>
</form>

وبهذا نرى أن الشيفرة الموجهة للمتصفحات يمكن كتابتها في صورة أجزاء batches أو في صورة حدَثية event driven، أو بهما معًا، وفق متطلبات كل حالة.

خاتمة

نأمل في هذا المقال أن تكون تعلمت ما يلي:

  • لا تبالي حلقات الأحداث بالأحداث التي تلتقطها.
  • تعالج معالجات الأحداث حدثًا واحدًا في كل مرة.
  • توفر أطر العمل -مثل Tkinter- حلقة أحداث، وبعض معالجات الأحداث الافتراضية أحيانًا.
  • يمكن كتابة الشيفرة لمتصفحات الويب بأسلوب مدفوع بالأحداث، أو بالأجزاء، أو بهما معًا.

ترجمة -بتصرف- للفصل الثامن عشر: Event Driven Programming، من كتاب Learn To Program لصاحبه Alan Gauld.

اقرأ أيضًا


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...