ابحث في المدونة

الأحد، 3 مايو 2020

تعلم كيف تقوم بتصميم برنامج القرعة الالكترونية بلغة البايثون

بسم الله الرحمن الرحيم
الحمد لله والصلاة والسلام على رسول الله وعلى آله وصحبه أجمعين.

وبعد فأيها الإخوة الكرام, إليكم مني أحر التحايا والسلام, فالسلام عليكم ورحمة الله وبركاته.
بدايةً مع سابق الاعتذار على مجيئه متأخرًا, أتقدم أليكم بأصدق التهاني بمناسبة شهر رمضان المبارك, سائلًا المولى جل في علاه أن يجعل نصيبنا منه الرحمة والمغفرة ورفعة الدرجات. إنه ولي ذلك والقادر عليه.

أما عن تدوينة اليوم فهي ستطلعكم على كيفية تصميم البرامج التي تقوم بالاختيار العشوائي بين الأسماء, تمامًا كما تسمعون عنها في المسابقات الإذاعية والتلفيزيونية التي تقو على القرعة.
برمجيًأ, فكرة البرنامج ليست صعبة جدًا ولكن الكود الذي سأعرضه لكم لا أخفيكم أنه عميق أكثر مما يجب.
السبب وراء ذلك أني قد قمت فيه بتضمين بعض الأفكار التي يمكن لكم استخدامها لإنشاء برامج أخرى كفكرة تخزين الإعدادات لاسترجاعها عند فتح البرنامج مرة أخرى , وكذلك تعاملنا مع مكتبة wx بشكل مفصل لتصميم واجهة البرنامج وجعلها أكثر ذكاءً

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

طبعًا قمت بالتعليق على بعض أجزاء الكود لتتعرفوا على أجزائه بسهولة, لكني لم أشرح كل سطر بالتفصيل لسببين أولهما كثرة الأسطر في الكود , والسبب الثاني هو أن هذه التدوينة موجهة في المقام الأول إلى الأشخاص الذين لديهم خلفية ولو بسيطة عن بايثون وكيف تُكتب.
ولعلنا بإذن الله سنفرد في تدوينات قادمة شروحات أكثر تفصيلًا ليستفيد لمن هو في صدد الدخول إلى مجال البرمجة.

والآن أترككم مع الكود

#هذه المكتبة خاصة بإدارة المجلدات والملفات
import os
# هذه مكتبة wx وتخص إنشاء الواجهات الرسومية
import wx
# هذه مكتبة لإجراء العمليات العشوائية
import random
#هذه المكتبة مسؤولة عن إنشاء الملفات التي نقوم فيها بتخزين إعدادات برنامجنا
import shelve

# إنشاء مجلد data الذي يتم فيه تخزين الأسماء لعرضها من جديد عند إعادة تشغيل البرنامج. وهنا تم استخدام أمر mkdir من مكتبة os للقيام بذلك وتم استثناء الخطأ FileExistsError الذي يظهر في حال كان المجلد موجودًا
try:
    os.mkdir("data")
except FileExistsError:
    pass
# الكلاس الرئيسي لنافذة البرنامج الأساسية
class main(wx.Frame):
    #جلب قائمة الأسماء المخزنة في ملف البيانات إن كانت موجودة وحفظها في متغير اسمه namesList ليتم استخدامه لاحقًا. وهذا المتغير تابع للكلاس , أي لا يمكن استدعاؤه إلا بكتابة self. داخل الكلاس , أو كتابة اسم الكلاس متبوعًا بنقطة بعدها اسم المتغير إن استخدمناه خارج الكلاس
    data = shelve.open("data\\data")
    if "names" in data.keys():
        namesList = data["names"]
    else:
        namesList = []
    data.close()
    # وضع العناصر الرئيسية للنافذة وربط القابل للضغط منها بدوال معينة
    def __init__(self):
        wx.Frame.__init__(self,parent=None,title="القرعة الالكترونية",name="القرعة الالكترونية",size=(400,450))
        self.Centre()
        panel = wx.Panel(self)
        self.namesBox = wx.ListBox(panel,-1,choices=[i[0]+", حالة الإجابة: "+i[1] for i in self.namesList if self.namesList != []],name="قائمة الأسماء: ",pos=(30,30,),size=(100,350))
        add = wx.Button(panel,-1,"إضافة...",pos=(335,30),size=(35,35))
        self.edit = wx.Button(panel,wx.ID_EDIT,"تحرير...",pos=(335,85),size=(35,35))
        self.remove = wx.Button(panel,wx.ID_REMOVE,"إزالة",pos=(335,120),size=(35,35))
        self.clear = wx.Button(panel,-1,"إفراغ",pos=(335,155),size=(35,35))
        add.Bind(wx.EVT_BUTTON,self.onAdd)
        add.SetDefault()
        self.clear.Bind(wx.EVT_BUTTON,self.onClear)
        self.edit.Bind(wx.EVT_BUTTON,self.onEdit)
        self.remove.Bind(wx.EVT_BUTTON,self.onRemove)
        self.Bind(wx.EVT_UPDATE_UI,self.onUpdate)
        menuBar = wx.MenuBar()
        mainMenu = wx.Menu()
        choose = mainMenu.Append(-1,"اسحب اسمًا\tCtrl+s")
        exit = mainMenu.Append(-1,"خروج\tCtrl+w")
        menuBar.Append(mainMenu,"القائمة الرئيسية")
        self.SetMenuBar(menuBar)
        self.Bind(wx.EVT_MENU,self.onChoose,choose)
        self.Bind(wx.EVT_MENU,self.onExit,exit)
        self.SetBackgroundColour(wx.BLUE)
        self.Show()
    #تعريف دالة الحدث التي تتنفذ في حال الضغط على زر إضافة ويتم فيها إظهار محاورة إضافة الاسم التي تم إنشاؤها لاحقًا في الكود
    def onAdd(self,event):
        addDLG.Show()
        addDLG.nameField.SetFocusFromKbd()
    # تعريف دالة choose التي تتنفذ عند الضغط على العنصر "اسحب اسمًا" حيث تم في هذه الدالة فرز الأسماء التي تم الإشارة إليها بأنها أجابت إجابة صحيحة ثم التأكد أن القائمة ليست فارغة والقيام بعد ذلك بالاختيار العشوائي بينها وإظهار رسالة تخبر المستخدم بالاسم الذي وقع اختياره من قبل الكمبيوتر
    def onChoose(self,event):
        names = [i[0] for i in self.namesList if i[1] == "صحيحة"]
        if names != []:
            choice = random.choice(names)
            wx.MessageBox("لقد وقع الاختيار على "+choice,self.GetTitle(),parent=self)
        else:
            wx.MessageBox("إن قائمة الأسماء فارغة",self.GetTitle(),parent=self)
    # دالة remove وتم فيها برمجة زر الحذف, حيث قمنا فيها بالتعرف على التحديد بصندوق الخيارات وحذفه من قائمة namesList وحفظ القائمة الجديدة في ملف data 
    def onRemove(self,event):
        index = self.namesBox.GetSelection()
        if index >= 0:
            self.namesList.pop(index)
            self.namesBox.Set([i[0]+", حالة الإجابة: "+i[1] for i in self.namesList if self.namesList != []])
            data = shelve.open("data\\data")
            data["names"] = self.namesList
            data.close()
            self.namesBox.SetFocusFromKbd()
            if len(self.namesList) >0:
                self.namesBox.SetSelection(index-1)
    # دالة edit والتي قمنا فيها ببرمجة زر التحرير, وعملية التحرير هنا تنقسم إلى قسمين, الأول في هذا الكلاس وتم فيه إخبار الكلاس الآخر الذي هو عبارة عن محاورة أننا نريد تحرير العنصر المحدد, فقمنا بتغيير عنوان المحاورة إلى "تحرير الاسم" , كما قمنا بتغيير اسم زر التنفيذ إلى "حفظ" , وهذه المحاورة هي أساسًا نفسها محاورة الإضافة , لكن من أجل عدم الاضطرار إلى إنشاء محاورة جديدة قمنا بهذه الحيلة لاستخدام نفس النافذة كمحاورة للإضافة والتحرير, وقد تم تعبئة خانة كتابة الاسم بالاسم المراد تحريره , ومربع التحديد الذي يشير إلى حالة الإجابة تم تغييره هو الآخر حسب حالة العنصر المراد تحويله أيضًا.
    def onEdit(self,event):
        index = self.namesBox.GetSelection()
        if index >= 0 :
            addDLG.nameField.SetValue(self.namesList[index][0])
            if self.namesList[index][1] == "صحيحة":
                addDLG.stateBox.SetValue(True)
            else:
                addDLG.stateBox.SetValue(False)
            addDLG.edit = True
            addDLG.SetTitle("تحرير الاسم")
            addDLG.SetName("تحرير الاسم")
            addDLG.add.SetLabel("حفظ")
            addDLG.Show()
            addDLG.nameField.SetFocusFromKbd()
    # برمجة دالة الحدث لزر الإفراغ. وتم فيه عرض رسالة للمستخدم تسأله ما إن كان متأكدًا من رغبته في إفراغ المحتويات. وقد تم تعيين قيمة namesList إلى قائمة فارغة وتخزينها في ملف البيانات عوضًا عن القائمة السابقة
    def onClear(self,event):
        msg = wx.MessageBox("هل أنت متأكد من رغبتك في إفراغ قائمة الأسماء؟ \n لا يمكن التراجع عن هذا الإجراء","إفراغ",parent=self,style=wx.YES_NO)
        if msg == 2:
            data = shelve.open("data\\data")
            data["names"] = self.namesList = []
            data.close()
            self.namesBox.Set([])
        self.namesBox.SetFocusFromKbd()
    # هذه الدالة عبارة عن فكرة جمالية خطرت في بالي , وهي مربوطة بحدث لا نهائي , أي أنه يتم تنفيذها باستمرار. وتقوم هذه الدالة بالتأكد ما إذا كانت قائمة الأسماء فارغة , وحينها يتم إخفاء كل من زر الحذف والتحرير والإفراغ كون أنه لا حاجة للمستخدم إليها, وبمجرد ما يقوم المستخدم بإضافة اسم إلى القائمة تظهر تلك الأزرار.
    def onUpdate(self,event):
        if self.namesList == []:
            self.remove.Hide()
            self.edit.Hide()
            self.clear.Hide()
        else:
            self.remove.Show()
            self.edit.Show()
            self.clear.Show()
    # دالة إغلاق البرنامج
    def onExit(self,event):
        wx.Exit()
# كلاس جديد. هو عبارة عن المحاورة التي تظهر للمستخدم عند الضغط على زر الإضافة في حين , وزر التحرير في حين آخر.
class addDLG(wx.Dialog): والحالة الافتراضية له من حيث عنوان المحاورة واسم الزر هو الإضافة
    # قمنا بحفظ العنوان الافتراضي للنافذة في متغير لإعادته من جديد
    defaultTitle = "إضافة اسم"
    defaultLabel = "إضافة"
    # هذا المتغير يتم تغييره من خلال الكلاس السابق عند الضغط على زر التحرير. ويتم تغيير القيمة إلى True بدل False . أما إن بقي False فسيدل ذلك على أن المحاورة لإضافة عنصر وليس للتحرير
    edit = False
    # وضع العناصر في النافذة وترتيبها
    def __init__(self):
        wx.Dialog.__init__(self,parent=main,title="إضافة اسم",name="إضافة اسم")
        self.Centre()
        panel = wx.Panel(self)
        self.nameField = wx.TextCtrl(panel,-1,value="",name="الاسم: ",size=(180,100),pos=(30,30))
        self.stateBox = wx.CheckBox(panel,-1,label="إجابة صحيحة",name="إجابة صحيحة")
        self.stateBox.SetValue(True)
        self.add = wx.Button(panel,-1,"إضافة")
        self.add.SetDefault()
        cansel = wx.Button(panel,-1,"إلغاء")
        self.add.Bind(wx.EVT_BUTTON,self.onAdd)
        cansel.Bind(wx.EVT_BUTTON,self.onCansel)
        self.Bind(wx.EVT_CHAR_HOOK,self.onEscape)
        self.Bind(wx.EVT_CLOSE,self.onExit)
    #هذه الدالة سنستخدمها لاحقًا لإرجاع إعدادات المحاورة كالعنوان ومحتوى المربع النصي وتسمية الزر إلى الوضع الافتراضي بعد الانتهائ من التحرير. كما يتم فيها إرجاع قيمة المتغير edit إلى False مرة أخرى
    def defaults(self):
        if self.edit == True:
            self.SetTitle(self.defaultTitle)
            self.SetName(self.defaultTitle)
            self.add.SetLabel(self.defaultLabel)
            self.edit = False
        self.stateBox.SetValue(True)
        self.nameField.SetValue("")
    # برمجة زر الإضافة والتحرير
    def onAdd(self,event):
    # أخذ قيمة المربع النصي
        name = self.nameField.GetValue()
    # إذا كان المربع ليس فارغًا
        if name != "":
            # إذا كانت قيمة edit هي False , فقم بإضافة محتوى المربع النصي إلى القائمة , وتحقق كذلك من حالة ال checkBox , فإذا كان محددًا فاضبط حالة الإجابة أنها صحيحة وإن كانت خاطئة فاضبطها على أنها خاطئة. ويتم هنا إضافة بيانات من نوع قائمة ذات خانتين. الخانة الأولى تحتوي على الاسم والخانة الثانية تحتوي على حالة الإجابة. فإذًا محتويات القائمة namesList هي عبارة عن قوائم فرعية في كل عنصر. وهنا أيضًا قمنا بإنشاء متغير داخل الدالة اسمه selection لضبط التحديد في صندوق الخيارات بعد الانتهاء من إضافة العنصر بدلًا من أن يتم التحديد على أول عنصر
            if self.edit == False:
                if self.stateBox.IsChecked():
                    main.namesList.append([name,"صحيحة"])
                else:
                    main.namesList.append([name,"خاطئة"])
                selection = len(main.namesList)-1
            # وإلا. أي إذا كانت edit تساوي True , فقم بإجرائات التحرير. وإجرائات التحرير هي أولًأ معرفة الاسم المحدد باستخدام الوظيفة GetSelection. وتغيير الخانة الأولى من القائمة الفرعية والتي تخزن فيها قيمة الاسم إلى التعديل الجديد. والخانة الثانية يتم فيها تحديث حالة الإجابة , وهنا سنضبط قيمة selection بحيث تساوي index التي هي عبارة عن ترتيب العنصر المحدد , وهنا نحن نجبر البرنامج أن لا يعود إلى بداية القائمة
            else:
                index = main.namesBox.GetSelection()
                main.namesList[index][0] = name
                if self.stateBox.IsChecked():
                    main.namesList[index][1] = "صحيحة"
                else:
                    main.namesList[index][1] = "خاطئة"
                selection = index
            # بعد كل تلك الإجراءات سننفذ دالة defaults لإرجاع كل شيء كما كان , ونقوم أيضًا بتخزين المحتوى الجديد سواء المضاف أو المعدل إلى الملف data وضبط خيارات صندوق الاختيارات إلى القائمة المحدثة
            self.defaults()
            dataFile = shelve.open("data\\data")
            dataFile["names"] = main.namesList
            dataFile.close()
            options = [i[0]+", حالة الإجابة: "+i[1] for i in main.namesList if main.namesList != []]
            main.namesBox.Set(options)
            # إخفاء المحاورة وضبط تركيز لوحة المفاتيح على صندوق الخيارات
            self.Hide()
            main.namesBox.SetFocusFromKbd()
            # ضبط التحديد بحيث يكون حسب ما هو مسجل في المتغير selection
            main.namesBox.SetSelection(selection)
        # أما إن كان مربع التحرير فارغًا فقم بإظهار رسالة تخبر المستخدم بأنه لا يمكن إضافة الاسم ما إن لم يتم تعبئة خانة كتابة الاسم
        else:
            wx.MessageBox("لا ينبغي أن يكون حقل كتابة الاسم فارغًا.","القرعة الالكترونية",parent=self)
    # برمجة زر الإلغاء , ويتم فيه تنفيذ الدالة defaults لإرجاع كل شيء إلى الحالة الافتراضية وإخفاء المحاورة
    def onCansel(self,event):
        self.Hide()
        self.defaults()
        main.namesBox.SetFocusFromKbd()
        # نفس هنا كنوع من الإضافة الجمالية قمنا بتكرار نفس أوامر الإلغاء ليتم تنفيذها عندما يقوم المستخدم بالضغط على زر الهروب escape
    def onEscape(self,event):
        if event.GetKeyCode() == 27:
            self.Hide()
            self.defaults()
            main.namesBox.SetFocusFromKbd()
        event.Skip()
    # تعيين ماذا يتم فعله عند إغلاق البرنامج. وهنا تم تنفيذ دالة wx.Exit التي تعني حرفيًأ الخروج من البرنامج
    def onExit(self,event):
        wx.Exit()
# إنشاء كائن wx.App الذي لا يمكن إنشاء نوافذ wx بدونه
app = wx.App()
# قمنا بإسناد الكلاسات إلى متغيرات. أي أننا أنشأنا كائنًأ لكل كلاس بحيث أننا لا نضطر إلى كتابة الأقواس عند رغبتنا في استخدام دالة أو متغير داخل الكلاس من الكلاس الآخر
main = main()
addDLG = addDLG()
# الحلقة اللا نهائية التي تنتظر منا نحن أن نقوم بإغلاق البرنامج. غير ذلك فإن البرنامج يبقى قيد التشغيل.
app.MainLoop()


ليست هناك تعليقات:

إرسال تعليق

قل شيئًا