教程-Griffon:使用Groovy构建桌面应用程序

发布于:2021-02-01 10:10:20

0

218

0

griffon groovy java javafx jvm sql swing

如果您愿意在其中加入一些Groovy,构建桌面应用程序可能是一种愉快的体验。Griffon是一个应用程序框架,它遵循Grails的精神,为桌面开发带来乐趣。

桌面应用程序开发,这个术语现在不像web开发那样为人所熟知,并发和并行也不是。然而,这并不意味着它已经死了。确实,在某些行业,桌面应用程序是解决特定问题的最佳选择;在其他一些环境中,出于安全原因,桌面应用程序是唯一的选择。想想金融机构、银行、健康产业、生物研究、化学实验室、卫星操作和军队;仅举几个例子。所有这些都对桌面应用程序优于web应用程序施加了一组特定的限制,例如安全性、对本地资源的访问、设备和端口通信。他们的另一个共同点是Griffon。是的,Griffon框架帮助所有这些行业和领域的团队完成了工作。

你可能听说过Griffon ,但仍然想知道它是什么。简而言之,它是JVM的桌面应用程序平台。它深深扎根于Groovy社区,因为这个项目是Groovy Swing团队的智囊团成员:Danno Ferrin、James Williams和我自己。尽管如此,如果我在解释Griffon的一些特征时有点激动的话,你可以原谅我,因为我非常喜欢这个项目。框架设计背后的一个关键驱动力是,Java开发人员应该很容易理解它。它还应该支持快速编码周期,同时保持源代码整洁。最后,生产力的提高和乐趣因素必须立即察觉。

出于这些原因,团队决定遵循Grails框架及其社区的步骤。两个框架之间有很多相似之处。例如,两者都有一个命令行界面,可以帮助您完成创建、构建、打包和部署应用程序的常规任务。两个框架都利用Groovy语言作为各自软件栈的粘合剂。工具集成也相当不错,因为主要ide和流行的文本编辑器为处理此类项目提供了良好的支持。

但理论已经够多了,让我们来练习一下吧!本文的其余部分将致力于构建一个简单的地址簿应用程序。我们肯定不会在剩下的几页中构建一个完整的应用程序,但我希望所有要讨论的事情都能给你足够的指导,让你继续使用这个框架。

设置和配置

第一步是在您的计算机上下载和配置Griffon;有几种选择。如果您从下载页面选择通用安装程序,它将解压二进制文件并为您配置路径环境,特别是在Windows平台上。或者,如果您在Linux机器上,您可以尝试使用RPM或基于Debian的软件包。ZIP或TGZ包可能是您最后的选择。只需下载软件包,将其解压缩到您选择的目录中,最好是没有空格的目录。接下来,配置一个环境变量GRIFFONu HOME,指向GRIFFON二进制发行版解包的目录。最后,确保PATH环境变量包含对GRIFFONu HOME/bin的引用。如果一切顺利,在–version标志打开的情况下调用griffon命令应该会显示与下面类似的输出。

{xunruicms_img_title}

初始步骤

首先,我们如何创建应用程序?通常,您可以选择基于Maven的方法并选择适当的原型来引导项目。或者,您仅可以简单地创建一个新目录,获取一些Ant脚本并使用它来完成。或者让您可信赖的IDE做出决定。选择,选择,选择。griffon命令行工具可以为您提供帮助。通过调用以下命令,每个Griffon应用程序都以相同的方式启动。

$ griffon create-app addressbook

$ cd addressbook

您会注意到输出中有一连串的行。如果需要,请继续检查新创建的应用程序的内容。create app命令通过创建几个目录和一些文件来初始化应用程序。其中一个目录是特别重要的,它的名字是格里芬应用程序。在这个目录中,您将找到另一组有助于保持源代码井然有序的目录。图1显示了刚才创建的griffon应用程序目录的扩展内容。

如您所知,Griffon利用MVC模式来安排组成应用程序的元素。创建应用程序后,您还将获得一个初始MVC组,其名称与应用程序的名称匹配。每个MVC成员中都有足够的代码来使应用程序运行。是的,信不信由你,该应用程序已准备好启动。返回控制台并执行以下命令。

$ griffon run-app

这应该编译应用程序源,程序包资源,汇总依赖关系并启动应用程序。在几秒钟内,您应该会看到一个弹出窗口, 如图2所示。

诚然,它看起来并不多,但是我们还没有编写任何代码!清单1显示了在打开文件 griffon-app / views / addressbook / AddressbookView.groovy 时可以找到的内容。

清单1–AddressbookView

package addressbookapplication(title: 'addressbook',  preferredSize: [320, 240],  pack: true,  //location: [50,50],   locationByPlatform:true,  iconImage: imageIcon('/griffon-icon-48x48.png').image,  iconImages: [imageIcon('/griffon-icon-48x48.png').image,               imageIcon('/griffon-icon-32x32.png').image,               imageIcon('/griffon-icon-16x16.png').image]) {    // add content here    label('Content Goes Here') // delete me}

我们在这里看到的是一种基于Swing的领域特定语言(简称DSL),它基于一种流行的Groovy特性:builders。在我们的特殊情况下,我们处理的是SwingBuilder。构建器只是一个节点和规则的集合,它们知道如何构建层次结构。Swing UI恰好由组件树组成。在视图中,我们可以观察到一个名为“application”的顶级节点以及应用于它的一些属性。接下来我们将看到一个名为“label”的子节点,其中包含一个文本条目。您可以识别代码结构,如图2所示。这就是Swing DSL的威力。代码和UI非常相似,在阅读DSL时很容易理解组件的结构。

构建UI

现在,我们已经在视图中看到了一些代码,让我们继续这个MVC成员,我们稍后将介绍另外两个。本着保持简单的精神,我们将更新UI,使其外观如图3所示。

清单2–AddressbookView更新

package addressbookapplication(title: 'Addressbook',  pack: true,  resizable: false,  locationByPlatform:true,  iconImage: imageIcon('/griffon-icon-48x48.png').image,  iconImages: [imageIcon('/griffon-icon-48x48.png').image,               imageIcon('/griffon-icon-32x32.png').image,               imageIcon('/griffon-icon-16x16.png').image]) {    menuBar {        menu('Contacts') {            controller.griffonClass.actionNames.each { name ->                menuItem(getVariable(name))            }                    }    }    migLayout(layoutConstraints: 'fill')    list(model: eventListModel(source: model.contacts),          constraints: 'west, w 180! ',         border: titledBorder(title: 'Contacts'),         selectionMode: ListSelectionModel.SINGLE_SELECTION,         keyReleased: { e ->  // enter/return key             if (e.keyCode != KeyEvent.VK_ENTER) return             int index = e.source.selectedIndex             if (index > -1) model.selectedIndex = index         },         mouseClicked: { e -> // double click             if (e.clickCount != 2) return             int index = e.source.locationToIndex(e.point)             if (index > -1) model.selectedIndex = index         })    panel(constraints: 'center', border: titledBorder(title: 'Contact')) {        migLayout(layoutConstraints: 'fill')        for(propName in Contact.PROPERTIES) {            label(text: GriffonNameUtils.getNaturalName(propName) + ': ',                        constraints: 'right')            textField(columns: 30, constraints: 'grow, wrap',                text: bind(propName, source: model.currentContact,                           mutual: true))        }    }    panel(constraints: 'east', border: titledBorder(title: 'Actions')) {        migLayout()        controller.griffonClass.actionNames.each { name ->            button(getVariable(name), constraints: 'growx, wrap')        }    }}

接下来我们将更新griffon app/models/addressbook中的模型/AddressbookModel.groovy地址簿模型. 在这里,我们将确保模型在内存中保留联系人列表;它还将保存对当前正在编辑的联系人的引用。我们将使用glazedlist(Swing开发人员中的一个流行选择)来组织联系人列表。清单3显示了构建列表和保留对当前编辑联系人的引用所需的所有代码。现在,联系人列表有一个与元素相关的特殊绑定。每当编辑一个元素时,它都会发布一个更改事件,列表将截获该事件;列表反过来会更新对列表更改感兴趣的人。回顾清单2,您可以看到列表定义使用了listEventModel节点。这正是一个组件,它将在更新可用时通知UI,从而重新绘制受影响的区域。我们只需要连接一对组件就可以实现!AddressbookModel使用两个特定于域的类:Contact和ContactPresentationModel。第一个可以看作是一个普通的域类,因为它用简单的属性定义了联系人应该是什么。后者是Contact类的可观察包装器。表示模型通常是能够支持绑定操作的装饰器。我们一会儿就来看看这两门课。

清单3

package addressbookimport groovy.beans.Bindableimport ca.odell.glazedlists.*import griffon.transform.PropertyListenerimport java.beans.PropertyChangeEventimport java.beans.PropertyChangeListenerclass AddressbookModel {    final EventListcontacts =              new ObservableElementList(        GlazedLists.threadSafeList(        new BasicEventList()),        GlazedLists.beanConnector(ContactPresentationModel)    )        final ContactPresentationModel currentContact = new ContactPresentationModel()        @PropertyListener(selectionUpdater)    @Bindable int selectedIndex = -1        private selectionUpdater = { e ->currentContact.contact = contacts[selectedIndex].contact    }        AddressbookModel() {        currentContact.addPropertyChangeListener(new ModelUpdater())    }        private class ModelUpdater implements PropertyChangeListener {        void propertyChange(PropertyChangeEvent e) {            if(e.propertyName == ‘contact’ || selectedIndex < 0) return            contacts[selectedIndex][e.propertyName] = e.newValue        }    }    void removeContact(Contact contact) {        currentContact.contact = null        ContactPresentationModel toDelete = contacts.find {             it.contact == contact         }        if(toDelete != null) contacts.remove(toDelete)    }}

但是在我们展示域之前,让我们先介绍一下最后一个MVC成员:控制器。控制器的工作是对用户输入做出反应并协调信息流。现在,我们只需要填空就可以使应用程序再次工作,例如,将清单4中所示的代码粘贴到griffon app/controllers/addresbook/A中ddressbookController.groovy地址。

清单4

package addressbookclass AddressbookController {    def model    void newAction(evt) { }    void saveAction(evt) { }      void deleteAction(evt) { }

你还记得模型在控制器和视图之间传递数据吗?这就是为什么控制器类上有一个model属性。Griffon提供了一种基本的依赖注入机制,只要在MVC成员各自的类中定义了某些属性,就可以保证每个MVC成员都可以与其他两个成员进行通信。

如果您是一名Grails开发人员,您可能已经注意到我们并没有从域建模开始,这是处理Grails应用程序时的常见情况。做出这种选择有两个原因。首先,向您展示MVC成员的基本知识以及它们如何相互作用。第二,Griffon不支持开箱即用的域类,至少不像Grails所理解的那样,也就是说,Griffon还没有gormapi。但是我们可以通过编写简单的域类来管理自己。清单5例如显示了Contact域类的外观。

清单5

package addressbook@groovy.transform.EqualsAndHashCodeclass Contact {    long id    String name    String lastname    String address    String company    String email        String toString() { "$name $lastname : $email" }        static final ListPROPERTIES = ['name', 'lastname',          'address', 'company', 'email']

这个类可以在src/main/addressbook文件中定义/联系人:groovy. 接下来我们将在src/main/addressbook/Cont中定义伴随的表示模型actPresentationModel.groovy公司清单6中的代码。

清单6

package addressbookimport groovy.beans.Bindable@griffon.transform.PropertyListener(propertyUpdater)class ContactPresentationModel {    // attributes    @Bindable String name    @Bindable String lastname    @Bindable String address    @Bindable String company    @Bindable String email        // model reference    @Bindable Contact contact = new Contact()        private propertyUpdater = { e ->if(e.propertyName == 'contact') {            for(property in Contact.PROPERTIES) {                def bean = e.newValue                delegate[property] = bean != null ? bean[property] : null            }        }    }    String toString() { "$name $lastname" }    void updateContact() {        if(contact) {            for(property in Contact.PROPERTIES) {                contact[property] = this[property]            }        }    }}

正如我们前面提到的,domain类的设计很简单;它只需要关注我们想要保留的数据。另一方面,表示模型通过具有相同的属性来镜像域类,但稍作修改:每个属性都是可观察的。这意味着只要这些属性中的任何一个的值发生更改,就会触发一个事件。这些事件是启用绑定的事件。Griffon使用@Bindable注释来指示Groovy编译器将一组指令注入字节码,从而使这个类成为可观察的类。@Bindable属于Groovy语言中的一组特殊接口,为字节码操作打开了大门。这一组称为AST变换。在这段代码中发现了另一个AST转换,它是@PropertyListener。这种转换是在特定类和属性上定义和附加PropertyChangeListener的一种奇特方法。在我们的例子中,我们附加了一个PropertyChangeListener,它对所有属性更改都做出反应。清单6中所有代码的下一个效果是,当Contact实例附加到ContactPresentationModels实例时,所有属性值都将从Contact复制到模型。当调用updateContact()方法时,反向操作将生效。

我们几乎准备好再次运行该应用程序。但是在我们这样做之前,我们必须安装一组插件,这将使我们的生活更轻松。我们说过我们将使用GlazedLists。该库由插件提供,因此我们将对其进行安装。在视图中,我们使用了MigLayout,因此我们还将为其安装一个插件。最后,我们将安装另一个插件,使基于控制器方法创建UI动作变得轻而易举。转到控制台提示符,然后键入以下命令:

$ griffon install-plugin glazedlists

$ griffon install-plugin miglayout

$ griffon install-plugin actions

保持联系

是时候完成申请了。我们前面有几项任务:

  • 填写每个控制器操作所需的代码

  • 将联系人列表保存到数据库

  • 确保在应用程序启动时从数据库加载联系人

填充动作是一项简单的操作,因为我们已经设置的绑定已经处理了大部分数据操作。清单7显示了AddressbookController的最终代码。 

清单7–AddressbookController

package addressbookclass AddressbookController {    def model    def storageService     void newAction(evt) {        model.selectedIndex = -1        model.currentContact.contact = new Contact()    }         void saveAction(evt) {        // push changes to domain object        model.currentContact.updateContact()        boolean isNew = model.currentContact.contact.id < 1        // save to db        storageService.store(model.currentContact.contact)        // if is a new contact, add it to the list        if(isNew) {            def cpm = new ContactPresentationModel()            cpm.contact = model.currentContact.contact            model.contacts << cpm        }    }         void deleteAction(evt) {        if(model.currentContact.contact && model.currentContact.contact.id) {            // delete from db            storageService.remove(model.currentContact.contact)            // remove from contact list            execInsideUIAsync {                model.removeContact(model.currentContact.contact)                model.selectedIndex = -1            }        }    }         void dumpAction(evt) {        storageService.dump()    }     void mvcGroupInit(Map args) {        execFuture {            List<ContactPresentationModel> list = storageService.load().collect([]) {                new ContactPresentationModel(contact: it)            }            execInsideUIAsync {                model.contacts.addAll(list)            }        }    }}

第一个操作newAction涉及重置当前选择(如果有的话)并创建一个空联系人。saveAction()应该将表示模型的更改推回到域对象,将数据存储在数据库中,如果这是一个新联系人,则将其添加到联系人列表中。请注意,在处理数据库问题时,我们将委托给另一个名为storageService的组件,在完成对控制器的描述后,我们将立即看到这个组件。第三个操作首先从数据库中删除联系人,然后从联系人列表中删除。我们添加了第四个操作,用于将数据库内容转储到控制台中。在代码中找到的最后一条信息与从数据库加载数据和填写联系人列表有关。方法名很特别,因为它是MVC生命周期的一个钩子。这个特殊的方法将在所有MVC成员被实例化之后被调用,可以把它看作一个成员初始值设定项。我们已经准备好查看storageService组件。 

Griffon中的服务只不过是普通的类,但它们从框架中得到了特殊的处理。例如,它们被视为单例,只要MVC成员定义了一个名称与服务名称匹配的属性,它们就会被自动注入到MVC成员中。利用这些知识,我们将创建一个StorageService类,如下所示:

$ griffon create-service storage

这将在griffon app/services/addressbook中创建一个文件/存储服务.groovy使用默认内容。清单8显示了应用程序必须放入该文件中才能工作的代码。

清单8

package addressbookclass StorageService {    List<Contact> load() {        withSql { dsName, sql ->            List tmpList = []            sql.eachRow('SELECT * FROM contacts') { rs ->                tmpList << new Contact(                    id:       rs.id,                    name:     rs.name,                    lastname: rs.lastname,                    address:  rs.address,                    company:  rs.company,                    email:    rs.email                )            }            tmpList        }    }     void store(Contact contact) {        if(contact.id < 1) {            // save            withSql { dsName, sql ->                String query = 'select max(id) max from contacts'                contact.id = (sql.firstRow(query).max as long) + 1                List params = [contact.id]                for(property in Contact.PROPERTIES) {                    params << contact[property]                }                String size = Contact.PROPERTIES.size()                String columnNames = 'id, ' + Contact.PROPERTIES.join(', ')                String placeHolders = (['?'] * size + 1)).join(',')                sql.execute("""insert into contacts ($columnNames)                   values ($placeHolders""", params)            }        } else {            // update            withSql { dsName, sql ->                List params = []                for(property in Contact.PROPERTIES) {                    params << contact[property]                }                params << contact.id                String clauses = Contact.PROPERTIES.collect([]) { prop ->                    "$prop = ?"                }.join(', ')                sql.execute("""update contacts                    set $clauses where id = ?""", params)            }        }    }         void remove(Contact contact) {        withSql { dsName, sql ->            sql.execute('delete from contacts where id = ?', [contact.id])        }    }         void dump() {        withSql { dsName, sql ->            sql.eachRow('SELECT * FROM contacts') { rs ->                println rs            }         }    }}

每个服务方法都使用一个名为withSql的方法。如果我们安装另一个插件,这个方法就可用了。让我们现在就这么做:

$ griffon install-plugin gsql

现在,我们在这个小应用程序中启用了 Groovy SQL支持。Groovy SQL是常规SQL之上的另一个DSL。使用它,您可以使用与对象和对象图非常相似的编程API进行SQL调用。实际上,您甚至可以应用Groovy闭包,Groovy字符串和其他Groovy技巧,如 StorageService 类 的实现中所示。在再次启动该应用程序之前,我们必须注意另外两项。我们必须告诉GSQL插件,必须将withSql 方法应用于服务。其次,我们必须定义数据库模式。如前所述,还没有GORM API,必须手动定义数据库模式。

通过编辑文件 griffon-app / conf / Config.groovy 并添加以下行来完成第一个任务。

griffon.datasource.injectInto = [‘service’]

通过在 griffon-app / resources / schema.ddl中 创建一个具有以下内容的文件来完成第二个任务:

DROP TABLE IF EXISTS contacts;


CREATE TABLE contacts(


id INTEGER NOT NULL PRIMARY KEY,


name VARCHAR(30) NOT NULL,


lastname VARCHAR(30) NOT NULL,


address VARCHAR(100) NOT NULL,


company VARCHAR(50) NOT NULL,


email VARCHAR(100) NOT NULL


);

如何用一些初始数据为数据库播种呢?在Grails中,这是通过编辑名为 BootStrap.groovy的文件来完成的;在Griffon中,这是通过编辑 griffon-app / conf / BootstrapGsql.groovy来完成的。让我们向contacts表添加一个条目,如清单9所示。  

<span style="font-family: Courier, 'Courier New', monospace;">import groovy.sql.Sqlclass BootstrapGsql {    def init = { String dataSourceName = 'default', Sql sql ->        def contacts = sql.dataSet('contacts')        contacts.add(            id: 1,            name: 'Andres',            lastname: 'Almiray',            address: 'Kirschgartenstrasse 5 CH-4051 Switzerland',            company: 'Canno Engineering AG',            email: 'andres.almiray@canoo.com'        )    }     def destroy = { String dataSourceName = ‘default’, Sql sql ->    }} </span>

现在我们真的完成了。再次启动该应用程序。您应该在联系人列表中看到一个条目, 如图4 所示。用鼠标选择它,然后按 Enter 或双击它。这将使联系成为活动联系,并将其所有值置于中间形式。编辑其某些属性,然后单击“保存”按钮。创建一个新联系人并保存它。现在单击转储按钮。您应该在输出中看到与在联系人列表中可以找到的条目一样多的行。

这个应用程序的完整源代码可以在GitHub上找到。我们可以向这个应用程序添加更多的内容。例如,没有任何错误处理。如果SQL不是你喜欢的呢?没问题,您可以选择任何受支持的NoSQL选项。或者秋千不适合你。没问题,Griffon也支持SWT和JavaFX。更改UI工具包将意味着在保持其他组件几乎完好无损的情况下主要更改视图。你面前肯定有很多选择。