Tags: php gtk mvc framework 標籤語言 delphi
我們一般對 PHP 的印象是:寫 Web 應用程式的工具。其實它也可以作為單純的解譯器運行一般的本地程式, PHP 稱此運行模式為 CLI mode 。若進一步結合 PHP-GTK 擴充模組 (關於 PHP-GTK 的安裝,請參考《Glade/GTK2 for Windows with PHP5 and Ruby 快速安裝指南 》 ) ,我們仍然可以使用 PHP 設計具有圖形使用者介面的桌面應用程式。
本文不只單純地說明如何利用 PHP-GTK + Glade 設計桌面應用程式,更要混合現成的 Web 應用程式,一併為各位展示 MVC 架構所帶來的高度彈性與可用性。
PHP 是現今最流行的 Web 應用程式開發工具之一。與基於 MVC 架構的各種 framework 結合之後,開發速度與軟體品質也逐漸提昇。同時這也意味著,有愈來愈多現成的 Model 與 Control 組件可用。 MVC 架構在概念上就已經將使用者輸出入、資料來源、運算處理等等不同工作隔離了,其體質原本就可適應分散式架構。因此對 MVC 架構而言, Web 應用程式與桌面應用程式唯一的差異是使用者介面不同,也就是 View 不同。一但我們嘗試利用 PHP-GTK + Glade 設計桌面應用程式時,我們理論上將可直接使用那些原本是為 Web 應用程式所開發的 Model 與 Control 組件,僅需專注於基於 GTK 函數庫的圖形使用者介面之設計工作。
基於 MVC 架構的 Web 應用程式
在設計桌面應用程式之前,我們先來看一個基於 MVC 架構所設計的 Web 應用程式。這是一個現成的程式開發資源,我們稍候將直接利用此一現成的資源開發桌面應用程式,而且提供相同的操作功能。
Web 程式相關程式碼
本例中的 URL root 為 /test/FormExample
。
FormExample.php (web ver.)
<?php
require 'view/web/FormExampleView.php' ;
if ( isset ( $_SERVER [ 'PATH_INFO' ])) {
$pathInfoSet = explode ( '/' , $_SERVER [ 'PATH_INFO' ]);
if ( count ( $pathInfoSet ) < 3 ) {
echo "error...<br/> \n " ;
}
else {
list (, $controlName , $methodName ) = $pathInfoSet ;
$methodArgs = array_slice ( $pathInfoSet , 3 );
$controlName = ucfirst ( $controlName ) . 'Control' ;
require $controlName . '.php' ;
if ( class_exists ( $controlName )
and ( $control = new $controlName )
and method_exists ( $control , $methodName ))
{
$control -> results = call_user_func_array (
array ( $control , $methodName ),
$methodArgs
);
echo json_encode ( $control -> results );
}
exit ( 0 );
}
}
$view = new FormExampleView ;
$view -> show ( $control );
?>
UserControl.php
<?php
class UserControl {
protected $userTable = array (
'rock' => array ( 'userEmail' => 'rock@example.com' , 'userNote' => 'nothing' ),
'xman' => array ( 'userEmail' => 'xman@example.com' , 'userNote' => 'X' )
);
public function user_profile ( $userName ) {
return ( array_key_exists ( $userName , $this -> userTable )
? $this -> userTable [ $userName ]
: false
);
}
}
?>
view/web/FormExampleView.phtml
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<meta http-equiv= "content-type" content= "text/html; charset=utf-8" >
<title> Form Example</title>
<?php $this -> loadScript ( $this -> urlRoot . '/view/web/jquery.js' ); ?>
<!--script type="text/javascript" src="/test/view/web/jquery.js"></script-->
</head>
<body>
<?php $this -> loadScript ( $this -> urlRoot . '/view/web/FormExampleView.js' ); ?>
<style type= "text/css" >
#label {
border : none ;
width : 300px ;
height : 100px ;
}
</style>
<div id= "label" >
</div>
<form id= "form1" method= "post" >
<label for= "userName" > 姓名: </label>
<input id= "userName" name= "uesrName" type= "text" size= "40" />
<br/>
<label for= "userEmail" > E-Mail: </label>
<input id= "userEmail" name= "userEmail" type= "text" size= "40" />
<br/>
<label for= "userNote" > Note: </label>
<input id= "userNote" name= "userNote" type= "text" size= "40" />
<p>
<button type= "button" onclick= "on_submit_clicked();" > 送出</button>
<button type= "button" onclick= "on_close_clicked();" > 關閉</button>
</p>
</form>
</body>
</html>
view/web/FormExampleView.php (web ver.)
<?php
class FormExampleView {
public $urlRoot = '/test/FormExample' ;
public function loadScript ( $filePath ) {
echo '<script type="text/javascript" src="' ,
$filePath , '"></script>' , " \n " ;
}
public function show () {
require 'view/web/' . __CLASS__ . '.phtml' ;
}
}
?>
view/web/FormExampleView.js
function on_submit_clicked () {
var userName = jQuery ( 'input#userName' ) . val ();
var serviceUri = document . URL + '/user/user_profile/' + userName ;
//alert(serviceUri);
$ . getJSON ( serviceUri , function ( json ){
//alert("JSON Data: " + json.toString());
var s = '' ;
if ( json ) {
alert ( 'exist' );
s = 'userName: ' + userName + '<br/>' ;
for ( var p in json ) {
s += p + ': ' + json [ p ] + '<br/>' ;
}
}
else {
$ ( '#form1 :text' ) . each ( function ( i ){
s += this . id + ': ' + this . value + '<br/>' ;
});
}
$ ( '#label' ) . html ( s );
});
}
function on_close_clicked () {
window . close ();
}
設計桌面應用程式
首先,我們需要改造一下作為主控程式的 FormExample.php ,使其依 PHP 的運行模式判斷其所需的 View 。
以 PHP 內建的系統常數 PHP_SAPI 便可判斷運行模式。當 PHP_SAPI 之值為 'cli' 時,即為本地程式之運行狀態。
我於原本的 view 目錄下建立了 2 個子目錄,分別為 web 與 desktop ,各自放置 Web 應用程式的 View 源碼檔與桌面應用程式的 View 源碼檔。
FormExample.php (兩用版)
<?php
$viewType = ( PHP_SAPI == 'cli' ? 'desktop' : 'web' );
require 'view/' . $viewType . '/FormExampleView.php' ;
if ( isset ( $_SERVER [ 'PATH_INFO' ])) {
$pathInfoSet = explode ( '/' , $_SERVER [ 'PATH_INFO' ]);
if ( count ( $pathInfoSet ) < 3 ) {
echo "error...<br/> \n " ;
}
else {
list (, $controlName , $methodName ) = $pathInfoSet ;
$methodArgs = array_slice ( $pathInfoSet , 3 );
$controlName = ucfirst ( $controlName ) . 'Control' ;
require $controlName . '.php' ;
if ( class_exists ( $controlName )
and ( $control = new $controlName )
and method_exists ( $control , $methodName ))
{
$control -> results = call_user_func_array (
array ( $control , $methodName ),
$methodArgs
);
echo json_encode ( $control -> results );
}
exit ( 0 );
}
}
else {
require 'UserControl.php' ;
$control = new UserControl ;
}
$view = new FormExampleView ;
$view -> show ( $control );
?>
桌面程式相關程式碼
view/desktop/FormExampleView.glade
命名方式跟隨 Web 應用程式的慣例。並設主控視窗元件的名稱為 window ,以表示其意義如同 JavaScript 的 window 個體。
元件
事件
處理方法
window
destroy
Gtk::main_quit
submit
clicked
on_submit_clicked
close
clicked
on_close_clicked
內容過長,不線上顯示。
<? xml version = "1.0" standalone = "no" ?> <!--*- mode: xml -*-->
<!DOCTYPE glade-interface SYSTEM "http://glade.gnome.org/glade-2.0.dtd">
<glade-interface>
<widget class= "GtkWindow" id= "window" >
<property name= "width_request" > 400</property>
<property name= "height_request" > 300</property>
<property name= "visible" > True</property>
<property name= "title" translatable= "yes" > form example</property>
<property name= "type" > GTK_WINDOW_TOPLEVEL</property>
<property name= "window_position" > GTK_WIN_POS_NONE</property>
<property name= "modal" > False</property>
<property name= "resizable" > True</property>
<property name= "destroy_with_parent" > False</property>
<property name= "decorated" > True</property>
<property name= "skip_taskbar_hint" > False</property>
<property name= "skip_pager_hint" > False</property>
<property name= "type_hint" > GDK_WINDOW_TYPE_HINT_NORMAL</property>
<property name= "gravity" > GDK_GRAVITY_NORTH_WEST</property>
<property name= "focus_on_map" > True</property>
<property name= "urgency_hint" > False</property>
<signal name= "destroy" handler= "Gtk::main_quit" />
<child>
<widget class= "GtkVBox" id= "vbox1" >
<property name= "visible" > True</property>
<property name= "homogeneous" > False</property>
<property name= "spacing" > 0</property>
<child>
<widget class= "GtkLabel" id= "label" >
<property name= "visible" > True</property>
<property name= "label" translatable= "yes" > Hello</property>
<property name= "use_underline" > False</property>
<property name= "use_markup" > False</property>
<property name= "justify" > GTK_JUSTIFY_LEFT</property>
<property name= "wrap" > False</property>
<property name= "selectable" > False</property>
<property name= "xalign" > 0.5</property>
<property name= "yalign" > 0.5</property>
<property name= "xpad" > 0</property>
<property name= "ypad" > 0</property>
<property name= "ellipsize" > PANGO_ELLIPSIZE_NONE</property>
<property name= "width_chars" > -1</property>
<property name= "single_line_mode" > False</property>
<property name= "angle" > 0</property>
</widget>
<packing>
<property name= "padding" > 0</property>
<property name= "expand" > True</property>
<property name= "fill" > True</property>
</packing>
</child>
<child>
<widget class= "GtkTable" id= "form1" >
<property name= "border_width" > 5</property>
<property name= "visible" > True</property>
<property name= "n_rows" > 3</property>
<property name= "n_columns" > 2</property>
<property name= "homogeneous" > False</property>
<property name= "row_spacing" > 4</property>
<property name= "column_spacing" > 0</property>
<child>
<widget class= "GtkLabel" id= "label2" >
<property name= "visible" > True</property>
<property name= "label" translatable= "yes" > 姓名: </property>
<property name= "use_underline" > False</property>
<property name= "use_markup" > False</property>
<property name= "justify" > GTK_JUSTIFY_LEFT</property>
<property name= "wrap" > False</property>
<property name= "selectable" > False</property>
<property name= "xalign" > 0</property>
<property name= "yalign" > 0.5</property>
<property name= "xpad" > 0</property>
<property name= "ypad" > 0</property>
<property name= "ellipsize" > PANGO_ELLIPSIZE_NONE</property>
<property name= "width_chars" > -1</property>
<property name= "single_line_mode" > False</property>
<property name= "angle" > 0</property>
</widget>
<packing>
<property name= "left_attach" > 0</property>
<property name= "right_attach" > 1</property>
<property name= "top_attach" > 0</property>
<property name= "bottom_attach" > 1</property>
<property name= "x_options" > fill</property>
<property name= "y_options" ></property>
</packing>
</child>
<child>
<widget class= "GtkLabel" id= "label3" >
<property name= "visible" > True</property>
<property name= "label" translatable= "yes" > E-Mail: </property>
<property name= "use_underline" > False</property>
<property name= "use_markup" > False</property>
<property name= "justify" > GTK_JUSTIFY_LEFT</property>
<property name= "wrap" > False</property>
<property name= "selectable" > False</property>
<property name= "xalign" > 0</property>
<property name= "yalign" > 0.5</property>
<property name= "xpad" > 0</property>
<property name= "ypad" > 0</property>
<property name= "ellipsize" > PANGO_ELLIPSIZE_NONE</property>
<property name= "width_chars" > -1</property>
<property name= "single_line_mode" > False</property>
<property name= "angle" > 0</property>
</widget>
<packing>
<property name= "left_attach" > 0</property>
<property name= "right_attach" > 1</property>
<property name= "top_attach" > 1</property>
<property name= "bottom_attach" > 2</property>
<property name= "x_options" > fill</property>
<property name= "y_options" ></property>
</packing>
</child>
<child>
<widget class= "GtkLabel" id= "label4" >
<property name= "visible" > True</property>
<property name= "label" translatable= "yes" > Note: </property>
<property name= "use_underline" > False</property>
<property name= "use_markup" > False</property>
<property name= "justify" > GTK_JUSTIFY_LEFT</property>
<property name= "wrap" > False</property>
<property name= "selectable" > False</property>
<property name= "xalign" > 0</property>
<property name= "yalign" > 0.5</property>
<property name= "xpad" > 0</property>
<property name= "ypad" > 0</property>
<property name= "ellipsize" > PANGO_ELLIPSIZE_NONE</property>
<property name= "width_chars" > -1</property>
<property name= "single_line_mode" > False</property>
<property name= "angle" > 0</property>
</widget>
<packing>
<property name= "left_attach" > 0</property>
<property name= "right_attach" > 1</property>
<property name= "top_attach" > 2</property>
<property name= "bottom_attach" > 3</property>
<property name= "x_options" > fill</property>
<property name= "y_options" ></property>
</packing>
</child>
<child>
<widget class= "GtkEntry" id= "userName" >
<property name= "visible" > True</property>
<property name= "can_focus" > True</property>
<property name= "editable" > True</property>
<property name= "visibility" > True</property>
<property name= "max_length" > 0</property>
<property name= "text" translatable= "yes" ></property>
<property name= "has_frame" > True</property>
<property name= "invisible_char" > *</property>
<property name= "activates_default" > False</property>
</widget>
<packing>
<property name= "left_attach" > 1</property>
<property name= "right_attach" > 2</property>
<property name= "top_attach" > 0</property>
<property name= "bottom_attach" > 1</property>
<property name= "y_options" ></property>
</packing>
</child>
<child>
<widget class= "GtkEntry" id= "userEmail" >
<property name= "visible" > True</property>
<property name= "can_focus" > True</property>
<property name= "editable" > True</property>
<property name= "visibility" > True</property>
<property name= "max_length" > 0</property>
<property name= "text" translatable= "yes" ></property>
<property name= "has_frame" > True</property>
<property name= "invisible_char" > *</property>
<property name= "activates_default" > False</property>
</widget>
<packing>
<property name= "left_attach" > 1</property>
<property name= "right_attach" > 2</property>
<property name= "top_attach" > 1</property>
<property name= "bottom_attach" > 2</property>
<property name= "y_options" ></property>
</packing>
</child>
<child>
<widget class= "GtkEntry" id= "userNote" >
<property name= "visible" > True</property>
<property name= "can_focus" > True</property>
<property name= "editable" > True</property>
<property name= "visibility" > True</property>
<property name= "max_length" > 0</property>
<property name= "text" translatable= "yes" ></property>
<property name= "has_frame" > True</property>
<property name= "invisible_char" > *</property>
<property name= "activates_default" > False</property>
</widget>
<packing>
<property name= "left_attach" > 1</property>
<property name= "right_attach" > 2</property>
<property name= "top_attach" > 2</property>
<property name= "bottom_attach" > 3</property>
<property name= "y_options" ></property>
</packing>
</child>
</widget>
<packing>
<property name= "padding" > 0</property>
<property name= "expand" > False</property>
<property name= "fill" > False</property>
</packing>
</child>
<child>
<widget class= "GtkHButtonBox" id= "hbuttonbox1" >
<property name= "border_width" > 5</property>
<property name= "visible" > True</property>
<property name= "layout_style" > GTK_BUTTONBOX_SPREAD</property>
<property name= "spacing" > 0</property>
<child>
<widget class= "GtkButton" id= "submit" >
<property name= "visible" > True</property>
<property name= "can_default" > True</property>
<property name= "can_focus" > True</property>
<property name= "label" translatable= "yes" > 送出</property>
<property name= "use_underline" > True</property>
<property name= "relief" > GTK_RELIEF_NORMAL</property>
<property name= "focus_on_click" > True</property>
<signal name= "clicked" handler= "on_submit_clicked" />
</widget>
</child>
<child>
<widget class= "GtkButton" id= "close" >
<property name= "visible" > True</property>
<property name= "can_default" > True</property>
<property name= "can_focus" > True</property>
<property name= "label" translatable= "yes" > 關閉</property>
<property name= "use_underline" > True</property>
<property name= "relief" > GTK_RELIEF_NORMAL</property>
<property name= "focus_on_click" > True</property>
<signal name= "clicked" handler= "on_close_clicked" />
</widget>
</child>
</widget>
<packing>
<property name= "padding" > 0</property>
<property name= "expand" > False</property>
<property name= "fill" > False</property>
</packing>
</child>
</widget>
</child>
</widget>
</glade-interface>
view/desktop/View.php
設計 View 時,桌面應用程式有許多與 Web 應用程式之設計差異。在 Web 應用程式中,程序員並不需要處理使用者介面的諸多細節。但桌面應用程式就需要決定何時顯現視窗及視覺元件,何時進入等待使用者操作動作的迴圈,以及如何自視覺元件之中取得使用者輸入的資料。將這些細節封裝在此 View 基礎類。
<?php
class View extends GladeXML {
public function __construct ( $viewFilePath ) {
// $viewFilePath is requiried. Let PHP check it.
//echo $viewFilePath, "\n";
$args = func_get_args ();
call_user_func_array ( array ( 'GladeXML' , '__construct' ), $args );
//parent::__construct(...)
//name of default GtkWindow's instance
$this -> window = $this -> get_widget ( 'window' );
}
public function __get ( $widgetName ) {
return $this -> get_widget ( $widgetName );
}
public function show ( & $callBackControl ) {
$this -> control = $callBackControl ;
$this -> signal_autoconnect_instance ( $this );
Gtk :: main ();
}
public function alert ( $msg ) {
$msgDialog = new GtkMessageDialog ( $this -> window ,
Gtk :: DIALOG_MODAL , Gtk :: MESSAGE_ERROR , Gtk :: BUTTONS_OK , $msg );
$rc = $msgDialog -> run ();
$msgDialog -> destroy ();
return $rc ;
}
protected $inputGetMethodMap = array (
'GtkEntry' => 'get_text' ,
'GtkFileChooserButton' => 'get_filename'
);
public function & fetchFormValues ( $formName ) {
try {
$widgets = $this -> $formName -> get_children ();
$inputGetMethodMap = & $this -> inputGetMethodMap ;
foreach ( $widgets as $widget ) {
$gobj = $this -> get_widget ( $widget -> name );
$className = get_class ( $gobj );
if ( array_key_exists ( $className , $inputGetMethodMap )) {
//echo $widget->name, ': ', get_class($gobj), "\n";
$formValues [ $widget -> name ]
= $gobj -> $inputGetMethodMap [ $className ]();
}
}
}
catch ( Exception $e ) {
$formValues = false ;
}
return $formValues ;
}
}
?>
view/desktop/FormExampleView.php
這是桌面程式的 FormExampleView.php 。 Web 版載入 FormExampleView.phtml ,而桌面版則載入 FormExampleView.glade 。至於事件處理方法,在此處的 PHP 程式碼工作,與 Web 版中的 FormExampleView.js 中的 JavaScript 程式所負責的工作相同。
<?php
require 'View.php' ;
class FormExampleView extends View {
public function __construct () {
//load $path/$className.glade
parent :: __construct (
substr ( __FILE__ , 0 , strrpos ( __FILE__ , DIRECTORY_SEPARATOR ))
. DIRECTORY_SEPARATOR . __CLASS__ . '.glade'
);
}
public function on_close_clicked () {
Gtk :: main_quit ();
}
public function on_submit_clicked () {
$form =& $this -> fetchFormValues ( 'form1' );
print_r ( $form );
if (( $profile = $this -> control -> user_profile ( $form [ 'userName' ]))) {
foreach ( $profile as $k => $v ) {
$form [ $k ] = $v ;
}
$this -> alert ( 'exist' );
}
$s = '' ;
foreach ( $form as $k => $v ) {
$s . = " { $k } : { $v } \n " ;
}
$this -> refresh_label ( $s );
}
public function refresh_label ( $text ) {
$this -> label -> set_text ( $text );
}
}
?>
程式碼即文件。本文儘可能地精簡程式碼內容,只使用恰好能展示本文目的之內容。請各位自行閱讀源碼,細細體會 (好吧,我承認我是懶得打字了)。
本文於設計桌面應用程式時,完全使用了原有的 Control 與 Model 組件的程式碼。儘管那些 Control 與 Model 原本是為了 Web 應用程式所設計,但本文仍然未做任何修改便可運用如常。在設計桌面應用程式時所需要做的,就是專注於 View 的內容。本文除了示範如何實作一個封裝 GladeXML 的 View 基礎類,亦適切地展示了 MVC 架構的設計彈性與可用性。
樂多舊網址: http://blog.roodo.com/rocksaying/archives/3400303.html