作者:Benedikt Terhechte,原文鏈接,原文日期:2015-12-08
譯者:小鍋;校對:Prayer;定稿:
現(xiàn)在 Swift 已經(jīng)開源了,應(yīng)該有很多感興趣的用戶已經(jīng)在他們的 Linux 系統(tǒng)上安裝把玩過了。然而,目前的 Foundation 框架還在緊鑼密鼓地開發(fā)中,所以如果你想開發(fā)出比命令行更復(fù)雜一點的程序,就得鏈接已有的 Linux 庫,如 GTK, Cairo 或者 libpng 以開發(fā)出圖形界面的程序。
我剛剛實現(xiàn)了一個簡單的 Swift 程序,這個程序鏈接了 Unix 中最基本的 UI 類庫1 —— X11。在這個簡短的教程中,我會向你展示如果完成一個簡單的 X11 應(yīng)用程序,然后你就可以利用這些知識去鏈接到其它的類庫。
在開發(fā)的過程中,我們也會使用到新的 Swift 包管理器來為 X11 類庫創(chuàng)建一個簡單并且可重用的包。
下面是程序運行后的截圖2:
Swift 包管理器
在實際開始開發(fā)我們的 X11 應(yīng)用之前,需要先定義一個 Swift 包用來定義鏈接到 X11 類庫。一旦完成這個步驟,之后我們就可以將這個包分發(fā)給其它開發(fā)者,或者在別的項目中進行重用。我們必須將定義一個包和使用一個包區(qū)分開來。讓我們先從定義開始,然后再學(xué)習(xí)如何使用。
定義一個包
新建一個目錄用來保存我們的包。由于我們需要鏈接到 C 語言的庫,我遵循了 Swift 包管理器文檔的指南,為包名加上一個 C 的前綴,將包命名為 CX11.swift
bash
mkdir CX11.swift
對我們來說,我們不想寫任何的 Swift 包裝(wrapper)代碼,而是要直接對已有的 C 語言 API 進行鏈接。要鏈接到 C 語言類庫和頭文件需要通過 module.modulemap 文件,這個文件里包含了幫助 Swift 編譯器來進行正確的鏈接操作的必要指令信息。可以在這里獲取 modulemap 的語法文檔。創(chuàng)建一個 module map 文件,并且對其進行編輯(你可以自行選擇編輯器,在這里我使用了 nano):
bash
touch CX11.swift/module.modulemap
nano CX11.swift/module.modulemap
X11 是一個包含了很多功能的大型類庫。你可以在 /usr/include/X11 目錄下查看到它的內(nèi)容。在我們的示例中,我們不需要包含所有的頭文件,相對地,我們只需要用到其中的兩個: Xlib.h 和 X.h。對于其它的類庫,你可能需要包含更多的頭文件。在下面的內(nèi)容在我會提到如何用一種簡便方法來包含某個目錄下的全部頭文件。除了包含的頭文件,我們還需要告訴 Swift 鏈接哪一個庫??梢允褂?link 關(guān)鍵字來完成。我們的 modulemap 文件看起來應(yīng)該是這樣的:
module CX11 [system] {
module Xlib {
header "/usr/include/X11/Xlib.h"
}
module X {
header "/usr/include/X11/X.h"
}
link "X11"
}
我們將所創(chuàng)建的模塊(module)命名為 CX11,并且我們創(chuàng)建了兩個子模塊(submodules)。一個是 Xlib,另一個是 X。每一個子模塊定義了它需要導(dǎo)入的頭文件。最后我們使用 link 語句來鏈接到 libx11 類庫。
但是如果我們想鏈接到不止一個頭文件呢?Module maps 允許我們定義一個 umbrella 頭文件或指定一個 umbrella 目錄。
Umbrella Header 這是一個頭文件,里面包含了引用(通過 #include)其它頭文件的指令。一個好的示例是 <Cocoa/Cocoa.h> 或 <Foundation/Foundation.h> 還有 <gtk/gtk.h>。使用 Umbrella 關(guān)鍵字來定義 Umbrella 頭:
umbrella header "/usr/include/gtk/gtk.h"
Umbrella Directory 有時候你有一個頭文件目錄但是并沒有一個 umbrella header。在這種情況下,你可以告訴 Swift 直接到該目錄下查找頭文件:
umbrella "/usr/include/X11/"
除了 modulemap 文件,我們還需要一個 Package.swift 文件,否則我們的構(gòu)建將會失敗。但是這個文件可以是空的:
bash
touch CX11.swift/Package.swift
Swift 包管理器使用了 Git 和 Git Tags 來對包進行管理。所以我們還需要為我們的包創(chuàng)建一個 Git 倉庫,添加所有的文件,然后打上一個版本標簽。這是相當容易的:
bash
cd CX11.swift
git init
git add .
git commit -m "Initial Import"
git tag 1.0.0
cd ..
上述命令首先切換到目錄中,創(chuàng)建一個 Git 倉庫,添加所有的文件到倉庫中,提交,最后為這個提交添加一個版本標記(1.0.0)。
就是這些,我們的包已經(jīng)定義完成了,那我們應(yīng)該如何來使用呢?
包的使用
要使用一個包,我們要先定義一個 Package.swift 文件,這個文件可以告訴 Swift 需要導(dǎo)入哪些包到我們的項目中。但是首先得為我們的項目創(chuàng)建一個目錄。
bash
mkdir swift-x11
touch swift-x11/Package.swift
touch swift-x11/main.swift
需要注意的是(針對這個特定的示例程序)需要將 swift-x 目錄與 CX11.swift 放到同一個目錄:
bash
ls -l
CX11.swift
swift-x11
在真正著手開始寫 Swift 代碼與 X11 進行交互之前,我們需要告訴 swift-x11 項目如何導(dǎo)入 CX11 包。在 swift-x11/Package.swift中輸入這些代碼,我會在下面具體解釋這些代碼的意思:
import PackageDescription
let package = Package(
dependencies: [
.Package(url: "../CX11.swift", majorVersion: 1)
]
)
這些代碼的意思是告訴 Swift 要到 ../CX11.swift 目錄中查找包。
url(正如它的名字所代表的)不需要是一個本地的url,我已經(jīng)將我的 CX11.swift 上傳到了 GitHub,你也可以在 url 中直接使用 GitHub 的鏈接:
bash
import PackageDescription
let package = Package(
dependencies: [
.Package(url: "https://github.com/terhechte/CX11.swift.git", majorVersion: 1)
]
)
使用X11
現(xiàn)在我們已經(jīng)定義好了 X11 包,并且包管理器也配置完畢,現(xiàn)在就開始動手用 Swift 寫第一個 X11 程序吧。
這里有一個問題我沒辦法解決,那就是定義在 X11 頭文件中的宏并沒有導(dǎo)入到 Swift 當中。Xlib.h 文件當中定義了很多類似下面這樣的宏:
c
#define RootWindow(dpy, src) (ScreenOfDisplay(dpy, src)->root)
#define ScreenOfDisplay(dpy, scr)(&((_XPrivDisplay)dpy)->screens[scr])
因為這些宏都沒有被導(dǎo)入,所以我決定將宏所定義的完整代碼都寫出來。以下所有的代碼都是寫在 main.swift 文件當中的。你也可以在 GitHub 上找到最終完成版。請注意,這是一個簡單并且內(nèi)存不安全的示例程序。這僅僅是為了展示如果在 Linux 下使用 C 語言類庫。同時,我的 X11 知識也十分有限。我在 Linux 下使用 X11 編程已經(jīng)是 10 年前的事情了,我已經(jīng)忘了大半,因此下面的代碼解釋中可能存在錯誤。如果你發(fā)現(xiàn)了錯誤,盡管在這個倉庫中向我提交一個PR :)
我們先從導(dǎo)入前面定義的 CX11 類庫開始:
import CX11.Xlib
import CX11.X
配置
在這之后,我們需要定義一些變量。
- 我們需要一個 X11 顯示(大致來講就是與 X11 服務(wù)器的連接),這個變量命名為
d。 - 我們需要為創(chuàng)建的 X11 窗口(window)提示一個占位符,這個變量命名為
w。 - 我們還需要為 X11 服務(wù)器開辟一些內(nèi)存用于存儲 X11 輸入事件,這個變量命名為
e。 - 我們還需要保存要在窗口上顯示的文本,這個變量命名為
msg。 - 我們需要一個位置用來儲存當前的 X11 屏幕(一個 X11 顯示可以擁有多個屏幕),這個變量命名為
s。 - 最后,我們需要一個指針指向 X11 的根窗口,這個根窗口保存了其它的窗口,這個變量命名為
rootWindow。
// X11 Display
var d: _XPrivDisplay
// 我們將要創(chuàng)建的 window
var w: Window
// X11 產(chǎn)生的事件
var e = UnsafeMutablePointer<_XEvent>.alloc(1)
// 要顯示的文本內(nèi)容
var msg = "Hello Swift World"
// 指向當前 X11 Screen 的指針
var s: UnsafeMutablePointer<Screen>
變量定義好之后,我們需要打開到 X11 服務(wù)器的連接。但是,由于用戶有可能在沒有安裝 X11 服務(wù)器(比如,控制臺模式)的機器上運行這個應(yīng)用程序,所以我們需要判斷這個連接是否成功:
d = XOpenDisplay(nil)
if d == nil {
fatalError("cannot open display")
}
在成功打開連接之后,我們要獲取當前的默認顯示屏以及當前的根窗口。由于 RootWindow 宏不可用4,所以我們直接獲取 C 結(jié)構(gòu)體 的內(nèi)存區(qū)域。然而,因為當前的屏幕 s 是一個 UnsafeMutablePointer,我們需要增加一個 memory 屬性來獲得 root 實例。
// Get the default screen
s = XDefaultScreenOfDisplay(d)
// And the current root window on that screen
let rootWindow = s.memory.root
創(chuàng)建一個窗口
現(xiàn)在我們有了創(chuàng)建窗口以及將其顯示在屏幕中的所有東西。我們將使用 XCreateSimpleWindow 函數(shù)來完成。這個函數(shù)接受如下的參數(shù):
c
XCreateSimpleWindow(Display *display, Window parent, int x, int y,
unsigned int width, unsigned int height, unsigned int border_width,
unsigned long border, unsigned long background);
border 和 background 是顏色(color)值。為了避免手動創(chuàng)建顏色,我們簡單地傳入當前屏幕定義好的默認黑色和白色引用。這里需要再次用到 .memory 屬性。
// Create our window
w = XCreateSimpleWindow(d, rootWindow, 10, 10, 200, 100, 1,
s.memory.black_pixel, s.memory.white_pixel)
這段代碼會在 rootWindow 的 10/10 位位置創(chuàng)建一個寬度 200 和高度 100 的新窗口。邊框會是黑色,而背景將會是白色。
輸入事件
當然,我們還需要接受從 Xserver 上傳來的輸入事件。在這個例子中,我們需要知道窗口何時被顯示,此時我們可以在上面進行繪制,我們還需要知道用戶按下特定鍵退出程序的事件。第一個是 Expose 事件,第二個是 KeyPress 事件。接受事件需要通過 XselectInput 函數(shù)來注冊事件掩碼來完成:
c
XSelectInput(d, w, ExposureMask | KeyPressMask)
窗口創(chuàng)建完成之后,我們就可以顯示它了。這是通過 XMapWindow 函數(shù)來實現(xiàn)的:
c
XMapWindow(d, w)
事件循環(huán)(Event Loop)
最后,我們需要在窗口的顯示期間啟動事件循環(huán)。在這里,我用一個 while 循環(huán)來不斷地使用 XNextEvent 函數(shù)以獲取新的 X11 事件。接著,我們對事件進行判斷,以確定其是否為 Expose 以及 KeyPress 事件5。我們通過 swift 語句來進行判斷:
loop: while true {
// Wait for the next event
XNextEvent(d, e)
switch e.memory.type {
// The window has to be drawn
case Expose:
// draw a small black rectangle
XFillRectangle(d, w, s.memory.default_gc, 20, 20, 10, 10)
// draw the text
XDrawString(d, w, s.memory.default_gc, 10, 70, msg, Int32(msg.characters.count))
// The user did press a key
case KeyPress:
break loop
// We never signed up for this event
default: fatalError("Unknown Event")
}
}
這里的 e 事件結(jié)構(gòu)體還是一個 UnsafeMutablePointer,所以我們還是需要通過 memory 屬性來獲得真實的結(jié)構(gòu)體。Expose 事件表明現(xiàn)在窗口已經(jīng)可見,所以我們需要對進行重繪。這里的繪制十分簡單:使用 XFillRectangle 來繪制一個小的黑塊,以及 XDrawString 來將已初始化的 msg 文本繪制到窗口的 10, 70 位置。我不是很清楚 X11 是接受 unicode 還是 ascii 編碼,所以 Int32(msg.characters.count) 可能是錯的,但是在這個例子當中它可以正常運行。
另一個事件,KeyPress 一旦在用戶按下一個鍵的時候跳出外層的 while 循環(huán)并退出程序。
運行
要運行這個程序,只需要 check out 倉庫(最好在Linux上進行)并且在目錄中運行如下命令:
bash
swift build
這個命令會 clone CX11.swift 包,并且在 .build/debug 目錄中構(gòu)建出二進制文件。
通過如下的命令來運行:
bash
.build/debug/swift-x11-example
這將會執(zhí)行二進制文件,一個小小的 X11 窗口將會出現(xiàn)在你的桌面上:
總結(jié)
這是一個相當?shù)暮唵蔚氖纠?,展示了如何?Linux 下使用 Swift 寫一個 X11 應(yīng)用程序。當然,這些知識同樣適用地鏈接到其它類庫的不同類型的應(yīng)用程序。這個教程同時也通過使用一個簡單的 X11 包闡述了 Swift 包管理器是如何工作的。
完整的 X11 應(yīng)用程序代碼可以在這里找到。
完成的 X11 包代碼可以在這里找到。
- 我一開始使用的是 GTK3, 但是沒辦法讓它運行起來
- 十分壯觀,不是么 :)
- "我們希望社區(qū)采用的約定是為這種模塊加上 C 的前綴然后與Swift模塊一樣使用駝峰命名。這樣就可以為其它更加'Swifty'的純C接口的包裝函數(shù)使用如JPEG這樣的命名"
- 參見上面的解釋,我沒辦法找出具體的原因
- 我們僅注冊過這兩個事件
本文由 SwiftGG 翻譯組翻譯,已經(jīng)獲得作者翻譯授權(quán),最新文章請訪問 http://swift.gg。