GUI mit Perl/Tk

XBM-Editor in Perl/Tk

XBM ist ein altes, monochromes Grafikformat. So alt, dass es keineswegs mehr von Standard-Grafikprogrammen verarbeitet werden kann. Praktischerweise hat bei den Perlmonks schon jemand einen XBM-Editor in Perl/Tk geschrieben. Dieser wurde etwas verfeinert und steht nun hier zur Verfügung. Und auf Github.

pTkXbmEdit - ein XBM-Editor in der vorherigen Version
download pTkXbmEditDownload pTkXbmEdit-Quellcode (als ZIP-Datei)

#!perl

package My::XBMEditor;

use strict;
use warnings;
use Moose;
use Moose::Util::TypeConstraints;
use curry;
use Tk 804;
use Tk::Menu;
use Tk::Text;
use Tk::Image;
use Tk::Dialog;
use Tk::StatusBar;
use Tk::ToolBar;
use Image::Xbm;
use Data::Dumper qw/Dumper/;

our $VERSION = '3.0';
$VERSION = eval $VERSION;

# use coerce to glue Moose's attributes on Tk's textvariables
subtype 'TkRef' => as 'ScalarRef';
coerce 'TkRef', from 'Str', via { my $r = $_; return \$r };

=head1 NAME

pTkXbmEdit - an XBM editor written in Perl/Tk

=head1 DESCRIPTION

Source: http://www.perlmonks.org/?node_id=163230
    downloaded 2017-04-27 11:04

=head1 ATTRIBUTES

=head2 mw

The Tk::MainWindow object for the GUI.

=cut

has 'mw' => (is => 'ro', isa => 'Tk::MainWindow', default => sub{
    my $mw = Tk::MainWindow->new(
        -background  => 'ghostwhite',
        -borderwidth => 1,
        -relief      => 'groove',
        -width       => 500,
        -height      => 500,
        -title       => 'pTkXbmEdit - a Xbm Editor written in Perl/Tk by crazyinsomniac',
    );
    return $mw;
});

=head2 canvas

This is the canvas where the grid is drawn.

=cut

has 'canvas' => (is => 'rw', isa => 'Tk::Canvas');

has 'about_dialog' => (is => 'ro', isa => 'Tk::Dialog', lazy => 1, default => sub{
    my $self = shift;
    my $mw = $self->mw;
    my $d = $mw->Dialog(
        -title => 'About pTkXbmEdit',
        -text => "This is pTkXbmEdit v$VERSION",
        -bitmap => 'info',
        -buttons => ['Dismiss'],
    );
    return $d;
});


=head2 status_sticky

Private attribute. Will hold some text that is sticky in the status bar.
Set to empty string if you want to clear the sticky text.

=cut

has 'status_sticky' => (is => 'rw', isa => 'Str', default => '');


=head2 status

Holds a reference to the text displayed in the status bar.

Note: dereference to set a new value, e.g. C<${$self->status} = 'new value';>

=cut

has 'status' => (is => 'rw', isa => 'TkRef', coerce => 1, default => '');


=head2 grid_width_x

Holds a reference to the width of the grid.

=cut

has 'grid_width_x' => (is => 'rw', isa => 'TkRef', coerce => 1, default => '');


=head2 grid_height_y

Holds a reference to the height of the grid.

=cut

has 'grid_height_y' => (is => 'rw', isa => 'TkRef', coerce => 1, default => '');


=head2 grid

=cut

has 'grid' => (is => 'rw', isa => 'ArrayRef[ArrayRef[Int]]', default => sub{ [[]] });


=head2 crazy0naCAMEL

Not sure about this. It's used when the XBM grisd is drawn for the very first time.

=cut

has 'crazy0naCAMEL' => (is => 'ro', isa => 'Str', default => "---------#-----------#--------
---------##------####---------
-----######-----#---#---------
---##---###-----##--#---------
--##########----#--#----------
---##--#####-----##-----------
---#---####------#-##---------
---#---####---####-#----------
---#--####---#-----#----------
---#--####----##-##-----------
---#-#########--######--------
---#-####---##--#######-------
---#-#######-#-#########------
---#--####################----
----#-#####################---
-----#######################--
-------#####################--
--------###################---
--------######------#######---
---------####-------######----
---------###---------#####----
---------###---------###-#----
---------###---------###-#----
---------###----------##-#----
---------###----------####----
--------##-----------###------
",);


=head2 crazy0naCAMEL

Not sure about this. It's used when clearing the canvas.

=cut

has 'MirrorCamel' => (is => 'ro', isa => 'Str', default => "----####-----##-----------------
---######---###-----------------
---######--##-##-##-------------
---######--#---#####------------
---######----####--#-------##---
----###-----##-##----######-###-
-------------#-#-#--##--------##
-#####---------#---#------------
##----#####----#-##-------------
----------#######---------------
-#-------------#----------------
-#---#--#-----------#-----##----
###-#-#-#----------###---####---
#--------#-----#--#############-
-#--------##------------######--
--#------#---------------#---#--
###-----#----------------#---#--
-#------#-----------------------
--------------------------------
--------------------------------
--------#---#-###--##-##----###-
--------#####-#--#-#-#-#---##-#-
###-------#---###--#-#-#--##--#-
-#-#-#--#####-#--#-#---#---#--#-
-#-##---#---#-#-#--#---#---#-##-
-#-#-#--#---#-##---#---#---#-#--
---------------------------##---
----------###-----#-#--#----#---
---------##-#--####---###-------
--------##-#---#-##-#--#----###-
---------###-#-####-#--#---##-#-
----------####-##-#-#--##---###-
",);


=head1 METHODS

=head2 run()

=cut

sub run {
    my $self = shift;
    
    $self->set_us_up_the_gui();
    $self->set_us_up_the_grid();

    $self->mw->MainLoop;

    return;
} # /run




sub duck {
    my $self = shift;
    my $widget = shift or die('Missing canvas');
    
    my $Ev = $widget->XEvent;
    my $s = $Ev->s;

    if ($s =~ m{^B1}ix) {    # button1 down (left button)
        $self->flip_square($widget, 0);
    } elsif ($s =~ m{^B3}ix) {    # button3 down (right button)
        $self->flip_square($widget, 1);
    }
    
    return;
} # /duck




sub set_us_up_the_menu_bar {
    my $self = shift;
    my $mw = $self->mw;
    
    my $menuitems = [
        [Cascade => "~File", -tearoff => 0, -menuitems =>
            [
                [Button => "L~oad", -accelerator => 'Ctrl+o', -command => $self->curry::f_open,],
                [Button => "Save ~As", -accelerator => 'Ctrl+a', -command => $self->curry::f_saveas,],
                [Separator => ""],
                [Button => "E~xit", -command => $self->curry::exit_program(),],
            ],
        ],
        [Cascade => "~Help", -tearoff => 0, -menuitems =>
            [
                [Button => "~About pTkXbmEdit", -command => sub{ $self->about_dialog->Show; },],
            ],
        ],
    ];
    
    my $menu = $mw->Menu(-menuitems => $menuitems);
    $mw->configure(-menu => $menu);

    # -- add bindings for global hotkeys as the Tk::Menu accellerators don't do anything.
    $mw->bind($mw, "<Control-o>" => $self->curry::f_open);
    $mw->bind($mw, "<Control-a>" => $self->curry::f_saveas);

    return;
} # /set_us_up_the_menu_bar




sub exit_program {
    my $self = shift;
    my $mw = $self->mw;
    
    # TODO: add one of those annoying "do you really want to exit" dialogs
    # with a picture of a sad looking cat
    
    $mw->destroy;
    exit(0);
} # /exit_program




sub f_open {
    my $self = shift;
    my $mw = $self->mw;
    
    my @ext = (
        ["X BitMap",	[qw/.xbm/]],
        ["All files",	[qw/∗/]],
    );
    
    my $file = $mw->getOpenFile(
        -filetypes => \@ext,
        -defaultextension => 'xbm',
        -initialdir       => '.',
        -title            => "Open File:",
    );
    
    if ($file) {
        $self->Statuss("loading $file ...\n", "[$file]");
        my $xbm_image = undef;
        eval{
            $xbm_image = Image::Xbm->new(-file => $file);
        };
        if ( $@ ) {
            my $d = $mw->Dialog(
                -title => 'Invalid XBM File!',
                -text => 'Error reading file: ' . $@,
                -bitmap => 'error',
                -buttons => ['Ok'],
                -default_button => 'Ok',
            );
            $d->Show;
            $self->Statuss("Invalid XBM file", "[$file]");
        }else{
            $self->draw_us_up_a_picture($xbm_image->as_string());
            $self->Statuss("", "[$file]");
        }
        
    } else {
        $self->Statuss("no file to load\n");
    }

    return;
}

sub Statuss {
    my $self = shift;
    my ($msg, $sticky) = @_;
    
    $self->status_sticky($sticky) if defined $sticky; # "" will be used to clear
    
    ${$self->status} = $self->status_sticky . ' ' . $msg;
    return;
}

sub f_saveas {
    my $self = shift;
    my $mw = $self->mw;
    
    my $file = $mw->getSaveFile(
        -defaultextension => ".xbm",
        -initialdir       => "./*.xbm",
        -title            => "Save File:",
    );
    
    if ($file and length $file > 0) {
        my $picXBM = &make_us_up_the_picture();
        if ( !$picXBM ) {
            $self->Statuss("no picture, nothing to save\n");
            return;
        }
        eval{
            $picXBM->save($file)
        };
        if ( $@ ) {
            my $d = $mw->Dialog(
                -title => 'Error Saving File!',
                -text => "Error saving file: $@",
                -bitmap => 'error',
                -buttons => ['Ok'],
                -default_button => 'Ok',
            );
            $d->Show;
            $self->Statuss("Error saving $file\n");
        }else{
            $self->Statuss("saved $file, looks great\n");
        }
        
    } else {
        $self->Statuss("no filename, can't save shit\n");
    }
    return;
} # /f_saveas




sub set_us_up_the_gui {
    my $self = shift;
    my $mw = $self->mw;
    
    $self->set_us_up_the_menu_bar;
    
    my $tb = $mw->ToolBar(
        -movable => 0,
        -side => 'top',
    );
    
    $tb->ToolButton (
        -text => 'Flip All Squares',
        -command => sub { $self->flip_em_all(); },
    );
    
    $tb->ToolButton (
        -text => 'White All Squares',
        -command => sub { $self->wipe_em_all('white'); },
    );
    
    $tb->ToolButton (
        -text => 'Black All Squares',
        -command => sub { $self->wipe_em_all('black'); },
    );
    
    $tb->separator;
    
    $tb->ToolLabEntry(
        -label => 'X:',
        -labelPack => [-side => "left", -anchor => "w"],
        -bg => 'white',
        -textvariable => $self->grid_width_x,
    );
    
    $tb->ToolLabEntry(
        -label => 'Y:',
        -labelPack => [-side => "left", -anchor => "w"],
        -bg => 'white',
        -textvariable => $self->grid_height_y,
    );
    
    $tb->separator;
    
    $tb->ToolButton (
        -text => 'Make the grid',
        -command => sub { $self->set_us_up_the_grid(); },
    );
    
    $tb->ToolButton (
        -text => 'Clear the grid',
        -command => sub { $self->clear_us_up_the_grid(); },
    );
    
    ${$self->status} = "status";
    
    my $sb = $mw->StatusBar();
    $sb->addLabel(
        -relief => 'flat',
        -textvariable => \$self->status,
    );

    my $canvas = $mw->Canvas(
        -width      => 0,
        -height     => 0,
        -background => "#AFFAAF",
    )->pack;
    $self->canvas( $canvas );
    
    $canvas->CanvasBind('<ButtonPress>' => [$self->curry::flip_square()]);
    $canvas->CanvasBind('<Motion>' => [$self->curry::duck()]);
    
    return;
} # /set_us_up_the_gui




sub clear_us_up_the_grid {
    my $self = shift;
    my $canvas = $self->canvas;
    $self->Statuss(" ... clearing up the grid ... ", "");

    # if there isn't a grid, draw one
    if ( $#{$self->grid->[0]} == -1 ) {
        $self->draw_us_up_a_picture($self->MirrorCamel);
        return;
    }
    
    # if there is a grid, remove it

    $canvas->delete('all');
    $canvas->configure(-width => 0, -height => 0);

    $self->grid([[]]);

    $self->Statuss(" ... done clearing up the grid.");

    return;
} # /clear_us_up_the_grid




sub make_us_up_the_picture {
    my $self = shift;
    my $canvas = $self->canvas;
    $self->Statuss(' ... making up the picture ... ');

    my $picture = '';

    if ( $#{$self->grid->[0]} > -1 ) {
        for my $row (@{ $self->grid }) {
            for my $id (@{$row}) {
                $picture .= ($canvas->itemcget($id, -fill) eq 'white') ? '-' : '#';
            }
            $picture .= "\n";
        }
        $self->Statuss(' ... done making up the picture.');

        return Image::Xbm->new_from_string($picture);
    }

    $self->Statuss(' there was no grid to make a picture from.');

    return;
} # /make_us_up_the_picture




sub set_us_up_the_grid {
    my $self = shift;
    my ($x, $y, $scale) = @_;
    my $mw = $self->mw;
    
    $self->Statuss(" ... setting up the grid ...", "");

    $scale = 8 unless $scale;

    unless ($x and $y) {
        $x = ${$self->grid_width_x};
        $y = ${$self->grid_height_y};
    }

    ${$self->grid_width_x} = '';
    ${$self->grid_height_y} = '';

    $self->draw_us_up_a_picture($self->crazy0naCAMEL) unless $x and $y;

    $x =~ s/\D//;
    $y =~ s/\D//;

    $x =~ s/(.{2}).*$/$1/;
    $y =~ s/(.{2}).*$/$1/;

    return unless $x and $y;

    ${$self->grid_width_x} = $x if $x;
    ${$self->grid_height_y} = $y if $y;

    $self->clear_us_up_the_grid() if $#{$self->grid->[0]} > -1;
    
    my $canvas = $self->canvas;
    $canvas->configure(
        -width      => ($x + 2) * $scale,
        -height     => ($y + 2) * $scale,
        -background => "#AFFAAF",
    );

    my $grid = $self->layout_canvas_grid($self->canvas, $x, $y, $scale);
    $self->grid($grid);

    $self->Statuss(" ... done setting up the grid.");

    return;
} # /set_us_up_the_grid




=head2 layout_canvas_grid( $canvas, $colsX, $rowsY, $scale )

Creates a set of rectangles on the C<$canvas>.
C<$colsX> is the number of columns, C<$rowsY> is the number of rows. 
C<$scale> is the width of a single rectabgle.

=cut

sub layout_canvas_grid {
    my $self = shift;
    my ($widget, $colsX, $rowsY, $scale) = @_;

    $scale = 10 unless $scale;

    my @danums = ();

    for my $iy (1 .. $rowsY) {
        my @dabums = ();
        for my $ix (1 .. $colsX) {
            my $id = $widget->create(
                'rectangle',
                $ix * $scale,
                $iy * $scale,
                $ix * $scale + $scale,
                $iy * $scale + $scale,
                -fill => 'white',
            );

            $widget->itemconfigure($id);
            push @dabums, $id;
        }
        push @danums, \@dabums;
    }

    return \@danums;
} # /layout_canvas_grid




sub wipe_em_all {
    my $self = shift;
    my $color = shift or die('Missing color');
    die('Color must be one of: black, white') if $color !~ m/^black|white$/;
    my $canvas = $self->canvas;
    
    # assumtion: there is nothing else on the canvas
    $canvas->itemconfigure('all', -fill => $color,);
    
    return;
} # /wipe_em_all




sub flip_em_all {
    my $self = shift;
    my $canvas = $self->canvas;
    
    for my $row (@{ $self->grid }) {
        for my $id (@{$row}) {
            my $color = $canvas->itemcget($id, -fill);
            $color = ($color eq 'white') ? 'black' : 'white';
            $canvas->itemconfigure($id, -fill => $color,);
        }
    }
} # /flip_em_all




sub flip_square {
    my $self = shift;
    my $canvas = shift or die('Missing canvas');
    my $draw_black_or_white = shift; # may be undef
    
    my $Ev = $canvas->XEvent;
    my $x = $Ev->x;
    my $y = $Ev->y;
    
    my $id = $canvas->find('overlapping', $x, $y, $x + 1, $y + 1);

    if ($id) {
        my $color = $canvas->itemcget($id, -fill);
        if (defined $draw_black_or_white) {
            if ($draw_black_or_white) {
                $color = 'white';
            } else {
                $color = 'black';
            }
        } else {
            $color = (defined($color) && $color eq 'white') ? 'black' : 'white';
        }

        $canvas->itemconfigure($id, -fill => $color,);
    }
    return;
} # /flip_square




=head2 draw_us_up_a_picture( $string )

Draws a pircture from C<$string>.

=cut

sub draw_us_up_a_picture {
    my $self = shift;
    my $string = shift or die('Missing string to draw');
    
    my $xbm = Image::Xbm->new_from_string($string);
    my @picrow = map { [ split '', $_ ] } split "\n", $xbm->as_string();

    return unless @picrow;

    my ($xbm_height, $xbm_width) = $xbm->get( '-height', '-width' );
    
    $self->set_us_up_the_grid($xbm_width, $xbm_height);

    my $canvas = $self->canvas;
    for (my $iy = 0 ; $iy < $xbm_height ; $iy++) {
        for (my $ix = 0 ; $ix < $xbm_width ; $ix++) {
            $canvas->itemconfigure(
                $self->grid->[$iy][$ix],
                -fill => 'black',
            ) unless $picrow[$iy][$ix] ne '#';
        }
    }

    return;
} # /draw_us_up_a_picture




=head1 TODOs / Plans

I<I can't wait to do this in Wx already.> (crazyinsomniac)

rewrite in Wx eventually
(ought to speed everything up, makes adding scrollbars easier)

but first add undo/redo, and

the behaviour so button_down/button_up does a bind/unbind of

Motion (rather than listening all the time)
and add for configurable [SQUARE] sizes ('em little boxes which flip)
that way you can cram a 500 x 500 xbm on screen ;)

add Ctrl-s (save buffer to file) Ctrl-a ( save as)

=cut

1; # /My::XBMEditor

package main;

use strict;
use warnings;

my $app = My::XBMEditor->new;
$app->run;
exit(0);

So sah der XBM-Editor im Originalzustand aus. Übrigens: das mittlerweile 12 Jahre alte Skript war nach nur marginalen Änderungen schon lauffähig.

pTkXbmEdit - ein XBM-Editor in der vorherigen Version

Top