生命不止,繼續 go go go!!!
golang官方並沒有提供Windows gui庫,但是今天還是要跟大家分享一下使用golang開發Windows桌面程式,當然又是面向github程式設計了。
知乎上有一個問答:golang為什麼沒有官方的gui包?
這裡,主要使用第三方庫lxn/walk,進行Windows GUI程式設計。
lxn/walkgithub地址:https://github.com/lxn/walk
star:2018
描述:A Windows GUI toolkit for the Go Programming Language
獲取:
go get github.com/lxn/walk1
例子:
main.go
package mainimport ( "github.com/lxn/walk" . "github.com/lxn/walk/declarative" "strings")func main() { var inTE, outTE *walk.TextEdit MainWindow{ Title: "SCREAMO", MinSize: Size{600, 400}, Layout: VBox{}, Children: []Widget{ HSplitter{ Children: []Widget{ TextEdit{AssignTo: &inTE}, TextEdit{AssignTo: &outTE, ReadOnly: true}, }, }, PushButton{ Text: "SCREAM", OnClicked: func() { outTE.SetText(strings.ToUpper(inTE.Text())) }, }, }, }.Run()}12345678910111213141516171819202122232425262728293031
go build生成go_windows_gui.exe。
go_windows_gui.exe.manifest
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0"> <assemblyIdentity version="1.0.0.0" processorArchitecture="*" name="SomeFunkyNameHere" type="win32"/> <dependency> <dependentAssembly> <assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/> </dependentAssembly> </dependency> </assembly>123456789
執行結果:
什麼是manifest上面提到了manifest,這是幹什麼的呢?
https://msdn.microsoft.com/en-us/library/windows/desktop/aa375365(v=vs.85).aspx
介紹:Manifests are XML files that accompany and describe side-by-side assemblies or isolated applications. Manifests uniquely identify the assembly through the assembly’s assemblyIdentity element. They contain information used for binding and activation, such as COM classes, interfaces, and type libraries, that has traditionally been stored in the registry. Manifests also specify the files that make up the assembly and may include Windows classes if the assembly author wants them to be versioned. Side-by-side assemblies are not registered on the system, but are available to applications and other assemblies on the system that specify dependencies in manifest files.
是一種xml檔案,標明所依賴的side-by-side組建。
如果用VS開發,可以Set透過porperty->configuration properties->linker->manifest file->Generate manifest To Yes來自動建立manifest來指定系統的和CRT的assembly版本。
詳解觀察上面的manifest檔案:
<xml>這是xml宣告:
版本號----<?xml version="1.0"?>。這是必選項。 儘管以後的 XML 版本可能會更改該數字,但是 1.0 是當前的版本。編碼宣告------<?xml version="1.0" encoding="UTF-8"?>這是可選項。 如果使用編碼宣告,必須緊接在 XML 宣告的版本資訊之後,並且必須包含代表現有字元編碼的值。standalone表示該xml是不是獨立的,如果是yes,則表示這個XML文件時獨立的,不能引用外部的DTD規範檔案;如果是no,則該XML文件不是獨立的,表示可以用外部的DTD規範文件。1234567
<dependency>這一部分指明瞭其依賴於一個庫:
<assemblyIdentity>屬性裡面還分別是:type----系統型別,version----版本號,processorArchitecture----平臺環境,publicKeyToken----公匙12345
應用做一個巨醜無比的登入框這裡用到了LineEdit、LineEdit控制元件
package mainimport ( "github.com/lxn/walk" . "github.com/lxn/walk/declarative")func main() { var usernameTE, passwordTE *walk.LineEdit MainWindow{ Title: "登入", MinSize: Size{270, 290}, Layout: VBox{}, Children: []Widget{ Composite{ Layout: Grid{Columns: 2, Spacing: 10}, Children: []Widget{ VSplitter{ Children: []Widget{ Label{ Text: "使用者名稱", }, }, }, VSplitter{ Children: []Widget{ LineEdit{ MinSize: Size{160, 0}, AssignTo: &usernameTE, }, }, }, VSplitter{ Children: []Widget{ Label{MaxSize: Size{160, 40}, Text: "密碼", }, }, }, VSplitter{ Children: []Widget{ LineEdit{ MinSize: Size{160, 0}, AssignTo: &passwordTE, }, }, }, }, }, PushButton{ Text: "登入", MinSize: Size{120, 50}, OnClicked: func() { if usernameTE.Text() == "" { var tmp walk.Form walk.MsgBox(tmp, "使用者名稱為空", "", walk.MsgBoxIconInformation) return } if passwordTE.Text() == "" { var tmp walk.Form walk.MsgBox(tmp, "密碼為空", "", walk.MsgBoxIconInformation) return } }, }, }, }.Run()}12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970
效果:
TableView的使用這裡主要使用的是TableView控制元件,程式碼參考github:
package mainimport ( "fmt" "sort" "github.com/lxn/walk" . "github.com/lxn/walk/declarative")type Condom struct { Index int Name string Price int checked bool}type CondomModel struct { walk.TableModelBase walk.SorterBase sortColumn int sortOrder walk.SortOrder items []*Condom}func (m *CondomModel) RowCount() int { return len(m.items)}func (m *CondomModel) Value(row, col int) interface{} { item := m.items[row] switch col { case 0: return item.Index case 1: return item.Name case 2: return item.Price } panic("unexpected col")}func (m *CondomModel) Checked(row int) bool { return m.items[row].checked}func (m *CondomModel) SetChecked(row int, checked bool) error { m.items[row].checked = checked return nil}func (m *CondomModel) Sort(col int, order walk.SortOrder) error { m.sortColumn, m.sortOrder = col, order sort.Stable(m) return m.SorterBase.Sort(col, order)}func (m *CondomModel) Len() int { return len(m.items)}func (m *CondomModel) Less(i, j int) bool { a, b := m.items[i], m.items[j] c := func(ls bool) bool { if m.sortOrder == walk.SortAscending { return ls } return !ls } switch m.sortColumn { case 0: return c(a.Index < b.Index) case 1: return c(a.Name < b.Name) case 2: return c(a.Price < b.Price) } panic("unreachable")}func (m *CondomModel) Swap(i, j int) { m.items[i], m.items[j] = m.items[j], m.items[i]}func NewCondomModel() *CondomModel { m := new(CondomModel) m.items = make([]*Condom, 3) m.items[0] = &Condom{ Index: 0, Name: "杜蕾斯", Price: 20, } m.items[1] = &Condom{ Index: 1, Name: "傑士邦", Price: 18, } m.items[2] = &Condom{ Index: 2, Name: "岡本", Price: 19, } return m}type CondomMainWindow struct { *walk.MainWindow model *CondomModel tv *walk.TableView}func main() { mw := &CondomMainWindow{model: NewCondomModel()} MainWindow{ AssignTo: &mw.MainWindow, Title: "Condom展示", Size: Size{800, 600}, Layout: VBox{}, Children: []Widget{ Composite{ Layout: HBox{MarginsZero: true}, Children: []Widget{ HSpacer{}, PushButton{ Text: "Add", OnClicked: func() { mw.model.items = append(mw.model.items, &Condom{ Index: mw.model.Len() + 1, Name: "第六感", Price: mw.model.Len() * 5, }) mw.model.PublishRowsReset() mw.tv.SetSelectedIndexes([]int{}) }, }, PushButton{ Text: "Delete", OnClicked: func() { items := []*Condom{} remove := mw.tv.SelectedIndexes() for i, x := range mw.model.items { remove_ok := false for _, j := range remove { if i == j { remove_ok = true } } if !remove_ok { items = append(items, x) } } mw.model.items = items mw.model.PublishRowsReset() mw.tv.SetSelectedIndexes([]int{}) }, }, PushButton{ Text: "ExecChecked", OnClicked: func() { for _, x := range mw.model.items { if x.checked { fmt.Printf("checked: %v\n", x) } } fmt.Println() }, }, PushButton{ Text: "AddPriceChecked", OnClicked: func() { for i, x := range mw.model.items { if x.checked { x.Price++ mw.model.PublishRowChanged(i) } } }, }, }, }, Composite{ Layout: VBox{}, ContextMenuItems: []MenuItem{ Action{ Text: "I&nfo", OnTriggered: mw.tv_ItemActivated, }, Action{ Text: "E&xit", OnTriggered: func() { mw.Close() }, }, }, Children: []Widget{ TableView{ AssignTo: &mw.tv, CheckBoxes: true, ColumnsOrderable: true, MultiSelection: true, Columns: []TableViewColumn{ {Title: "編號"}, {Title: "名稱"}, {Title: "價格"}, }, Model: mw.model, OnCurrentIndexChanged: func() { i := mw.tv.CurrentIndex() if 0 <= i { fmt.Printf("OnCurrentIndexChanged: %v\n", mw.model.items[i].Name) } }, OnItemActivated: mw.tv_ItemActivated, }, }, }, }, }.Run()}func (mw *CondomMainWindow) tv_ItemActivated() { msg := `` for _, i := range mw.tv.SelectedIndexes() { msg = msg + "\n" + mw.model.items[i].Name } walk.MsgBox(mw, "title", msg, walk.MsgBoxIconInformation)}123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
效果:
檔案選擇器(加入了icon)這裡就是呼叫Windows的檔案選擇框主要是使用:FileDialog
package mainimport ( "github.com/lxn/walk" . "github.com/lxn/walk/declarative")import ( "fmt" "os")type MyMainWindow struct { *walk.MainWindow edit *walk.TextEdit path string}func main() { mw := &MyMainWindow{} MW := MainWindow{ AssignTo: &mw.MainWindow, Icon: "test.ico", Title: "檔案選擇對話方塊", MinSize: Size{150, 200}, Size: Size{300, 400}, Layout: VBox{}, Children: []Widget{ TextEdit{ AssignTo: &mw.edit, }, PushButton{ Text: "開啟", OnClicked: mw.pbClicked, }, }, } if _, err := MW.Run(); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) }}func (mw *MyMainWindow) pbClicked() { dlg := new(walk.FileDialog) dlg.FilePath = mw.path dlg.Title = "Select File" dlg.Filter = "Exe files (*.exe)|*.exe|All files (*.*)|*.*" if ok, err := dlg.ShowOpen(mw); err != nil { mw.edit.AppendText("Error : File Open\r\n") return } else if !ok { mw.edit.AppendText("Cancel\r\n") return } mw.path = dlg.FilePath s := fmt.Sprintf("Select : %s\r\n", mw.path) mw.edit.AppendText(s)}12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364
效果:
文字檢索器package mainimport ( "fmt" "log" "strings" "github.com/lxn/walk" . "github.com/lxn/walk/declarative")func main() { mw := &MyMainWindow{} if _, err := (MainWindow{ AssignTo: &mw.MainWindow, Title: "SearchBox", Icon: "test.ico", MinSize: Size{300, 400}, Layout: VBox{}, Children: []Widget{ GroupBox{ Layout: HBox{}, Children: []Widget{ LineEdit{ AssignTo: &mw.searchBox, }, PushButton{ Text: "檢索", OnClicked: mw.clicked, }, }, }, TextEdit{ AssignTo: &mw.textArea, }, ListBox{ AssignTo: &mw.results, Row: 5, }, }, }.Run()); err != nil { log.Fatal(err) }}type MyMainWindow struct { *walk.MainWindow searchBox *walk.LineEdit textArea *walk.TextEdit results *walk.ListBox}func (mw *MyMainWindow) clicked() { word := mw.searchBox.Text() text := mw.textArea.Text() model := []string{} for _, i := range search(text, word) { model = append(model, fmt.Sprintf("%d檢索成功", i)) } log.Print(model) mw.results.SetModel(model)}func search(text, word string) (result []int) { result = []int{} i := 0 for j, _ := range text { if strings.HasPrefix(text[j:], word) { log.Print(i) result = append(result, i) } i += 1 } return}123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778
效果:
郵件群發器別人寫的郵件群發器,出自:https://studygolang.com/articles/2078關於golang中stmp的使用,請看部落格:Go實戰–透過net/smtp傳送郵件(The way to go)
package mainimport ( "bufio" "encoding/gob" "errors" "fmt" "io" "net/smtp" "os" "strconv" "strings" "time")import ( "github.com/lxn/walk" . "github.com/lxn/walk/declarative")type ShuJu struct { Name string Pwd string Host string Subject string Body string Send string}func SendMail(user, password, host, to, subject, body, mailtype string) error { fmt.Println("Send to " + to) hp := strings.Split(host, ":") auth := smtp.PlainAuth("", user, password, hp[0]) var content_type string if mailtype == "html" { content_type = "Content-Type: text/html;charset=UTF-8" } else { content_type = "Content-Type: text/plain;charset=UTF-8" } body = strings.TrimSpace(body) msg := []byte("To: " + to + "\r\nFrom: " + user + "<" + user + ">\r\nSubject: " + subject + "\r\n" + content_type + "\r\n\r\n" + body) send_to := strings.Split(to, ";") err := smtp.SendMail(host, auth, user, send_to, msg) if err != nil { fmt.Println(err.Error()) } return err}func readLine2Array(filename string) ([]string, error) { result := make([]string, 0) file, err := os.Open(filename) if err != nil { return result, errors.New("Open file failed.") } defer file.Close() bf := bufio.NewReader(file) for { line, isPrefix, err1 := bf.ReadLine() if err1 != nil { if err1 != io.EOF { return result, errors.New("ReadLine no finish") } break } if isPrefix { return result, errors.New("Line is too long") } str := string(line) result = append(result, str) } return result, nil}func DelArrayVar(arr []string, str string) []string { str = strings.TrimSpace(str) for i, v := range arr { v = strings.TrimSpace(v) if v == str { if i == len(arr) { return arr[0 : i-1] } if i == 0 { return arr[1:len(arr)] } a1 := arr[0:i] a2 := arr[i+1 : len(arr)] return append(a1, a2...) } } return arr}func LoadData() { fmt.Println("LoadData") file, err := os.Open("data.dat") defer file.Close() if err != nil { fmt.Println(err.Error()) SJ.Name = "使用者名稱" SJ.Pwd = "使用者密碼" SJ.Host = "SMTP伺服器:埠" SJ.Subject = "郵件主題" SJ.Body = "郵件內容" SJ.Send = "要傳送的郵箱,每行一個" return } dec := gob.NewDecoder(file) err2 := dec.Decode(&SJ) if err2 != nil { fmt.Println(err2.Error()) SJ.Name = "使用者名稱" SJ.Pwd = "使用者密碼" SJ.Host = "SMTP伺服器:埠" SJ.Subject = "郵件主題" SJ.Body = "郵件內容" SJ.Send = "要傳送的郵箱,每行一個" }}func SaveData() { fmt.Println("SaveData") file, err := os.Create("data.dat") defer file.Close() if err != nil { fmt.Println(err) } enc := gob.NewEncoder(file) err2 := enc.Encode(SJ) if err2 != nil { fmt.Println(err2) }}var SJ ShuJuvar runing boolvar chEnd chan boolfunc main() { LoadData() chEnd = make(chan bool) var emails, body, msgbox *walk.TextEdit var user, password, host, subject *walk.LineEdit var startBtn *walk.PushButton MainWindow{ Title: "郵件傳送器", MinSize: Size{800, 600}, Layout: HBox{}, Children: []Widget{ TextEdit{AssignTo: &emails, Text: SJ.Send, ToolTipText: "待發送郵件列表,每行一個"}, VSplitter{ Children: []Widget{ LineEdit{AssignTo: &user, Text: SJ.Name, CueBanner: "請輸入郵箱使用者名稱"}, LineEdit{AssignTo: &password, Text: SJ.Pwd, PasswordMode: true, CueBanner: "請輸入郵箱登入密碼"}, LineEdit{AssignTo: &host, Text: SJ.Host, CueBanner: "SMTP伺服器:埠"}, LineEdit{AssignTo: &subject, Text: SJ.Subject, CueBanner: "請輸入郵件主題……"}, TextEdit{AssignTo: &body, Text: SJ.Body, ToolTipText: "請輸入郵件內容", ColumnSpan: 2}, TextEdit{AssignTo: &msgbox, ReadOnly: true}, PushButton{ AssignTo: &startBtn, Text: "開始群發", OnClicked: func() { SJ.Name = user.Text() SJ.Pwd = password.Text() SJ.Host = host.Text() SJ.Subject = subject.Text() SJ.Body = body.Text() SJ.Send = emails.Text() SaveData() if runing == false { runing = true startBtn.SetText("停止傳送") go sendThread(msgbox, emails) } else { runing = false startBtn.SetText("開始群發") } }, }, }, }, }, }.Run()}func sendThread(msgbox, es *walk.TextEdit) { sendTo := strings.Split(SJ.Send, "\r\n") susscess := 0 count := len(sendTo) for index, to := range sendTo { if runing == false { break } msgbox.SetText("傳送到" + to + "..." + strconv.Itoa((index/count)*100) + "%") err := SendMail(SJ.Name, SJ.Pwd, SJ.Host, to, SJ.Subject, SJ.Body, "html") if err != nil { msgbox.AppendText("\r\n失敗:" + err.Error() + "\r\n") if err.Error() == "550 Mailbox not found or access denied" { SJ.Send = strings.Join(DelArrayVar(strings.Split(SJ.Send, "\r\n"), to), "\r\n") es.SetText(SJ.Send) } time.Sleep(1 * time.Second) continue } else { susscess++ msgbox.AppendText("\r\n傳送成功!") SJ.Send = strings.Join(DelArrayVar(strings.Split(SJ.Send, "\r\n"), to), "\r\n") es.SetText(SJ.Send) } time.Sleep(1 * time.Second) } SaveData() msgbox.AppendText("停止傳送!成功 " + strconv.Itoa(susscess) + " 條\r\n")}123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
效果: