• בלוג
  • עמוד 9
  • טריק קטן שישפר פלאים את הקוד הגרפי שאתם כותבים בפרל

טריק קטן שישפר פלאים את הקוד הגרפי שאתם כותבים בפרל

19/11/2015

פוסט זה כולל טיפ קצר לעבודה יעילה עם Python. אם אתם רוצים ללמוד פייתון יותר לעומק אני ממליץ על קורס Python כאן באתר.
הקורס כולל עשרות שיעורי וידאו והמון תרגול מעשי וילמד אתכם Python בצורה מקצועית מההתחלה ועד הנושאים המתקדמים.

ספריית Tk הפכה לסטנדרט של פיתוח ממשק משתמש גרפי לסקריפטים בפרל, אך בגלל מודל המחלקות של פרל פיתוח ממשק גרפי מונחה עצמים יכול להרגיש מסורבל. כך נראית התבנית שלי לפיתוח מונחה עצמים ב Tk שכדאי גם לכם לאמץ.

1. נסיון ראשון: מחלקה עבור מסך

הרעיון של מחלקה עבור מסך מוכר גם מספריות גרפיות אחרות, וב Tk נתקלתי בו לראשונה בפייתון ומיד התאהבתי. היתרון הוא שכל הפונקציונאליות של המסך מרוכזת במקום אחד, אפשר לייצר מספר מופעים של כל מסך והקוד בנוי ללא משתנים גלובליים.

כך לדוגמא נראית גירסת פייתון למחלקה עבור יישום מונה לחיצות:

import Tkinter as tk

class Clicker:
    def __init__(self, root):
        self.counter = 0

        self.panel = tk.Label(root, text="Clicks: %d" % self.counter)
        self.btn   = tk.Button(root, text="Click Me", command=self.onclick)

        self.btn.pack()
        self.panel.pack()

    def onclick(self):
        self.counter += 1
        self.panel.configure(text="Clicks: %d" % self.counter)

root = tk.Tk()
app = Clicker(root)

root.mainloop()

היתרון בכתיב הוא שבפונקציה onclick אין צורך לחפש במשתנים גלובליים מהו הפאנל שאותו צריך לעדכן מאחר והפונקציה היא חלק ממחלקת המסך ולכן יכולה לגשת לכל אחד מהפקדים על המסך כשדה מידע.

מה קורה כשמנסים לתרגם את זה לפרל? נסיון תרגום ראשון יכול להיראות כך, אך זה כמובן לא עובד:

use strict;
use warnings;
use Tk;

package Clicker {
  use Mouse;

  has 'root', is => 'ro', required => 1;

  has 'counter', is => 'rw', isa => 'Num', default => 0;

  has 'panel', is => 'ro', builder => '_build_panel';
  has 'button', is => 'ro', builder => '_build_button';

  sub BUILD {
    my ($self) = @_;

    $self->panel->pack;
    $self->button->pack;
  }

  sub _build_panel  { shift->root->Label(-text => "Clicks: 0") }
  sub _build_button { shift->root->Button(-text => "Click Me", -command => \&onclick) }

  sub onclick {
    my ($self) = @_;
    $self->counter($self->counter() + 1);
    $self->panel->configure(-text => "Clicks: " . $self->counter());
  }
};

package main;

my $w = MainWindow->new;
my $app = Clicker->new(root => $w);

MainLoop;

לחיצה על הכפתור מציגה את הודעת השגיאה הבאה:

Tk::Error: Can't call method "counter" on an undefined value at app.pl line 27.
 Tk callback for .button
 Tk::__ANON__ at /Library/Perl/5.18/darwin-thread-multi-2level/Tk.pm line 251
 Tk::Button::butUp at /Library/Perl/5.18/darwin-thread-multi-2level/Tk/Button.pm line 175
 <ButtonRelease-1>
 (command bound to event)

 

2. נסיון שני: פונקציות קשורות

הסיבה לשגיאה היא העברת הפונקציה onclick לכפתור לצורך הפעלתה בהמשך לטיפול בלחיצה. הבעיה היא שבפרל (כמו ב JavaScript ובניגוד לפייתון) העברת ה this pointer נקבעת באופן דינמי בעת הפעלת מתודה של אוביקט ולא בזמן הגדרת הפונקציה. במלים אחרות הפונקציה onclick בפייתון תמיד תופעל עם האוביקט שיצר אותה בתור הערך self, אבל בפרל אופן ההפעלה קובע אם יועבר האוביקט הנכון. השורה הבאה תפעיל את הפונקציה עם הערך הנכון של self:

$self->onclick();

ואילו השורה הבאה תפעיל את הפונקציה ללא העברת ערך לפרמטר הראשון self:

my $f = \&onclick;
$f->();

הפרמטרים שמועברים לפונקציה נקבעים בעת הפעלתה ולא בעת יצירת הפונקציה.

אחרי שמבינים את הבעיה אפשר גם לראות את הטריק הפשוט לתיקונה, פונקציה שנקרא לה bind. הפונקציה תקבל פונקציה אחרת ואוביקט ותחזיר פונקציה חדשה שתמיד תפעיל את הפונקציה המקורית עם האוביקט והפרמטרים המבוקשים. כך נראה המימוש:

sub _bind {
  my ($func, $self, @params) = @_;
  return sub {
    $self->$func(@params, @_);
  }
}

והקוד המלא:

use strict;
use warnings;
use Tk;

sub _bind {
  my ($func, $self, @params) = @_;
  return sub {
    $self->$func(@params, @_);
  }
}

package Clicker {
  use Mouse;

  has 'root', is => 'ro', required => 1;

  has 'counter', is => 'rw', isa => 'Num', default => 0;

  has 'panel', is => 'ro', builder => '_build_panel';
  has 'button', is => 'ro', builder => '_build_button';

  sub BUILD {
    my ($self) = @_;

    $self->panel->pack;
    $self->button->pack;
  }

  sub _build_panel  { shift->root->Label(-text => "Clicks: 0") }
  sub _build_button {
    my $self = shift;
    $self->root->Button(-text => "Click Me", -command => ::_bind(\&onclick, $self))
  }

  sub onclick {
    my ($self) = @_;
    $self->counter($self->counter() + 1);
    $self->panel->configure(-text => "Clicks: " . $self->counter());
  }
};

package main;

my $w = MainWindow->new;
my $app = Clicker->new(root => $w);

MainLoop;

 

3. הערות

בפרל יש כבר פונקציה בשם bind הלקוחה מעולם הרשתות. זאת הסיבה שבחרתי לקרוא לפונקציה _bind. יש גם מודול שימושי בשם UnderscoreJS שאפשר למצוא ב CPAN שמספר מימוש לפונקציה זו בדיוק.

ספריית Tk עצמה מציעה ממשק לקשירת פונקציות בהפעלתן דרך כפתור. זה נראה כך:

  sub _build_button {
    my $self = shift;
    $self->root->Button(-text => "Click Me", -command => [\&onclick, $self])
  }

השימוש ב bind נותן פתרון כללי יותר ולכן חשוב להכיר אותו.