#!/usr/bin/perl use strict; use warnings; use Gtk2 '-init'; use Text::CSV; use utf8; # vorgebene Zeilentrenner: my %row_sep=( '\r\n' => "\x0D\x0A", '\r' => "\x0D", '\n' => "\x0A", ); # vorgebene Spaltentrenner: my %col_sep=( ';' => ';', ':' => ':', ',' => ',', '' => "\t", '' => ' ', ); # Speicher für die Tabelle my @data; #my $glade_file='csv_view.xml'; my $glade_file='__DATA__'; my $gui=GUI->new($glade_file); $gui->set_row_sep_names(sort keys %row_sep); $gui->set_col_sep_names(sort keys %col_sep); # Signale verbinden # wenn Datei ausgewählt # Zeilen und Spalteentrenner erraten $gui->signal_connect(file_select => sub{ my $file=pop(); return 1 unless(open(my $fh, '<', $file)); local $/=undef; # die richtigen Spalten und Zeilentrenner müssen # die häufigsten Zeichen aus der liste der möglichen Zeichen sein. # das ist etwas brute force, dafür aber einfach. # Zeilenenden my @row_symbols; while(my ($key,$value)=each(%row_sep)) { seek($fh, 0,0); my $cnt=0; my $b=''; my $buffer=''; while(read($fh,$b,4096)) { $buffer.=$b; $cnt+=$buffer=~s/.*?$value//gs; } push(@row_symbols,[$key,$cnt]); } @row_symbols=sort{$b->[1] <=> $a->[1] or length($b->[0]) <=> length($a->[0])}@row_symbols; my $best_row_sep; $best_row_sep=$row_symbols[0][0] if($row_symbols[0][1] > 0); # Spaltenspearatoren my @col_symbols; while(my ($key,$value)=each(%col_sep)) { seek($fh, 0,0); my $cnt=0; my $b=''; my $buffer=''; while(read($fh,$b,4096)) { $buffer.=$b; $cnt+=$buffer=~s/.*?$value//gs; } push(@col_symbols,[$key,$cnt]); } @col_symbols=sort{$b->[1] <=> $a->[1] or length($b->[0]) <=> length($a->[0])}@col_symbols; my $best_col_sep; $best_col_sep=$col_symbols[0][0] if($col_symbols[0][1] > 0); close($fh); # Werte in der GUI setzen $gui->set_separarors($best_row_sep,$best_col_sep) if($best_row_sep and $best_col_sep); }); # wenn die Datei geladen werden soll $gui->signal_connect(file_load => sub { my $file=pop() // ''; @data=(); $gui->reset_filds(); my ($rsep,$csep)=$gui->get_separarors(); my $row_sep=defined($rsep)?$row_sep{$rsep} // $rsep:''; my $col_sep=defined($csep)?$col_sep{$csep} // $csep:''; my $fh; unless(open($fh, '<', $file)) { $gui->alert("Kann Datei $file nicht öffnen ($!)"); return 1; } if(!$row_sep and !$col_sep) { $gui->alert("Zeilen- oder Spaltentrenner sind nicht definiert!"); return 1; } my @description; my $size=0; local $/=$row_sep; my $csv=Text::CSV->new({ eol => $row_sep, sep_char => $col_sep}); while(my $line=<$fh>) { chomp($line); # wenn eine der ersten Zeilen mit # beginnt # und ansonsten wie eine CSV zeile aussieht # ist es die Beschreibung der Spalten if(!@data and $line=~/^\s*#\s*([^$col_sep]+?(?:$col_sep[^$col_sep]*)*)\s*$/o) { $csv->parse($1); @description = $csv->fields(); } # Kommentare ignorieren next if($line=~/\s*#/); # zeile parsen $csv->parse($line); # alles in den Speicher push(@data,[substr($line,0,10)."...",$csv->fields()]); $size=@{$data[-1]} if(@{$data[-1]} > $size); } close($fh); # wenn keine Beschreibung gefunden wurden # dann sollen die Spalten Durchnummeriert werden @description=map{ sprintf('%03u',$_) }(1..$size-1) unless(@description); # die Daten in der GUI setzen $gui->set_entry_names(@description); $gui->set_rows(\@data); return 1; }); # eine Zeile soll angezeigt werden $gui->signal_connect(row_select => sub { my $pos=pop(); return 1 if($pos <0 or $pos >= @data); my $row=$data[$pos] || []; $gui->set_entry_text(@$row[1..$#$row]); return 1; }); # gui anzeigen $gui->show_all(); # alles ausführen $gui->run(); ######################################################################## ######################################################################## { package GUI; use strict; use warnings; use Gtk2; use Gtk2::SimpleList; our $VERSION='0.1'; # es soll von Gtk2::Builder geerbt werden # das sieht nicht so aus wie man es von perl kennt # das liegt daran, das wir es hier im hintergunde mit C zu tun haben # und da fäuft das ganze etwas anders # # es werden auch ein paar signale hinzugefügt, # die das Objekt später auslösen kann. # # file_select # wird emittiert, wenn eine Datei ausgewählt wurde # file_load # wird gesendet, wenn der "Aktualisieren"-Button gedrückt wurde # row_select # eine Zeile wurde ausgewählt # quit # das Programm soll beendet werden # use Glib::Object::Subclass Gtk2::Builder::, signals => { file_select => { flags => [qw/run-last/], param_types => [qw/Glib::String/], }, file_load => { flags => [qw/run-last/], param_types => [qw/Glib::String/], }, row_select => { flags => [qw/run-last/], param_types => [qw/Glib::Int/], }, quit => { flags => [qw/run-last/], param_types => [], }, }, properties => []; # eine neue Klasseninstanz (Objekt) erzeugen sub new { my $class=shift; my $file=shift // '__DATA__'; # den Konstrukter der ElternKlasse nutzen my $self=$class->SUPER::new(); # die Glade-XML Datei laden # Wenn die datei an das Script angehängt ist if($file eq '__DATA__') { local $/=undef; return undef unless($self->add_from_string()); close(main::DATA); } # aus Datei laden else { return undef unless($self->add_from_file($file)); } # Datei Auswahl aus CSV-Dateien beschränken my $filter=Gtk2::FileFilter->new(); $filter->add_mime_type('text/csv'); $self->file_choose->set_filter($filter); # Die Listen fertig machen # Zeilenliste my $rows=Gtk2::SimpleList->new_from_treeview( $self->get_object('tlist'), 'Nr.' => 'int', 'Text' => 'text', ); @{$rows->{data}}=(); $self->{rows}=$rows; # die aktuelle Auswahl my $entry=Gtk2::SimpleList->new_from_treeview( $self->get_object('tentries'), 'Name' => 'text', 'Value' => 'text', ); @{$entry->{data}}=(); $self->{entry}=$entry; # $self->{entry_names}=[]; # die Alertbox nur verstecken nicht löschen $self->alert_box->signal_connect (delete_event => \&Gtk2::Widget::hide_on_delete); # alle Funktionsanmen aus der GladeXML mit diesem Objekt verbinden $self->connect_signals($self); # fertiges Objekt zurück return $self; } # nützliche Vereinfacheungen sub GUI { return $_[0]; } sub main_win { return $_[0]->get_object('main_win'); } sub file_choose{ return $_[0]->get_object('bfile'); } sub quit { return $_[0]->get_object('bquit'); } sub rows { return $_[0]->{rows} } sub entry { return $_[0]->{entry} } sub alert_box { return $_[0]->get_object('dalert'); } sub alert_text { return $_[0]->get_object('alert_text'); } sub row_sep { return $_[0]->get_object('row_sep'); } sub col_sep { return $_[0]->get_object('col_sep'); } sub run { Gtk2->main; } sub show_all { my $self=shift; $self->main_win->show_all(); $self->file_choose->set_title('öffne CSV'); } sub set_entry_text { my $self=shift; my @data=@_; my $entry=$self->entry(); @{$entry->{data}}=(); for my $name (@{$self->{entry_names}}) { my $text=shift(@data) // ''; push(@{$entry->{data}},[$name,$text]); } } sub set_entry_names { my $self=shift; @{$self->{entry_names}}=@_; } sub set_rows { my $self=shift; my $list=shift; my $data=$self->rows->{data}; @$data=(); for my $pos (0..$#$list) { push(@$data,[$pos,$list->[$pos]->[0]]); } } sub set_separarors { my $self=shift; my $rsep=shift; my $csep=shift; return (undef,undef) unless(defined($rsep) && defined($csep)); $self->_set_combobox_entry($self->row_sep(),$rsep); $self->_set_combobox_entry($self->col_sep(),$csep); } sub set_row_sep_names { my $self=shift; $self->_set_combobox_names($self->row_sep(),@_); } sub set_col_sep_names { my $self=shift; $self->_set_combobox_names($self->col_sep(),@_); } sub get_separarors { my $self=shift; my $csep=$self->col_sep()->get_active_text(); my $rsep=$self->row_sep()->get_active_text(); return ($rsep,$csep); } sub get_file { return $_[0]->file_choose->get_filename(); } sub reset_filds { my $self=shift; @{$self->rows->{data}}=(); @{$self->entry->{data}}=(); @{$self->{entry_names}}=(); } sub alert { my $self=shift; my $text=shift // ''; $self->alert_text()->set_text($text); $self->alert_box()->show_all(); } #----------------------------------------------------------------------- # privat #----------------------------------------------------------------------- sub _set_combobox_entry { my $self=shift; my $combo_box=shift; my $sep=shift; my $cnt=0; $combo_box->get_model()->foreach(sub{ my ($model, $path, $iter)=@_; my $txt=$model->get($iter); if($txt eq $sep) { $combo_box->set_active($cnt); return 1; } $cnt++; return 0; }); } sub _set_combobox_names { my $self=shift; my $combo_box=shift; my @list=@_; my $model = Gtk2::ListStore->new ('Glib::String'); $model->set($model->append, 0, $_) for(@list); $combo_box->set_model($model); my $renderer = Gtk2::CellRendererText->new(); $combo_box->pack_start($renderer, 1); $combo_box->add_attribute($renderer, text => 0); } #----------------------------------------------------------------------- # verbundene Funktionen: #----------------------------------------------------------------------- sub on_alert_ok { my $self=pop; $self->alert_text()->set_text(''); $self->alert_box()->hide(); return 0; } sub on_file_load { my $self=pop; $self->signal_emit('file_load',$self->get_file()); } sub on_file_select { my $self=pop(); $self->signal_emit('file_select',$self->get_file()); } sub on_row_select { my $self =pop(); my $tree_view =shift(); my $path =shift(); my $column =shift(); my $row = $self->rows->get_row_data_from_path($path); $self->signal_emit('row_select',$row->[0]); } sub on_exit { my $self=pop(); $self->signal_emit('quit'); Gtk2->main_quit; } 1;} package main; __DATA__ dialog-information True 5 vertical 5 True 3 2 5 5 True 1 2 GTK_FILL True 1 Datei: GTK_FILL GTK_FILL True 1 Zeilentrenner: 1 2 GTK_FILL GTK_FILL True 1 Spaltentrenner: 2 3 GTK_FILL GTK_FILL True 1 2 1 2 True 1 2 2 3 False 0 gtk-refresh True True True True False 1 True True True True False True True True True True 2 gtk-quit True True True True False 3 5 Ein Fehler ist aufgetreten True dialog-error normal True vertical 2 True True 0 0 gtk-dialog-error 6 False 0 True False 0 0 LEER True 5 1 0 True end gtk-ok True True True True False False 0 False end 1 button1