2023年7月31日 星期一

momo 買書兩本 : Azure WordPress 與深度學習

momo 66 折活動今天截止, 買了下面兩本深智的 :




雖然第二本母校可借, 但總是看一個月就被預約走, 趁 66 折買下來吧. 兩書合計 1359 元, 滿 1200 登記送 10% momo 幣 (那是 136 元媽?). 

2023年7月30日 星期日

2023 年第 30 周記事

本周只上了三天班, 由於杜蘇芮來襲放了兩天颱風假 (週五早上正要上班時還好有看手機), 趁著撿到的颱風假待在家, 把公司要用的第二套自動化軟體收尾寫完, 等下周一上班測試 OK 就算是全部完工了 (這樣到退休都可輕鬆駕馭, 高枕無憂了, 哈哈哈). 

放了兩天假, 週五傍晚雨勢較歇就先回鄉下了, 周末這兩天帶了兩包書回去其實都沒看 (帶安心的), 時間都花在看 6/20 就已上線的 Hahow 學校 Streamlit 線上課程. 過去的一個多月工作太忙, 連回到家還是繼續寫工作上要用的軟體, 實在沒時間. 這兩天我一口氣把 9 個章節全部看完, 嗯, 覺得還不錯, 但學習者必須有資料科學基礎較適合.


今天中午小舅來摘了兩袋龍眼要送人, 我原以為果肉還太薄不夠成熟,, 甜度也不夠, 打算下周才開始採收, 但我錯了, 我上完線上課程出去曬穀場看看採得如何, 發現其實較高處向陽的龍眼已經很甜很多肉了, 看樣子等到下個周末回去鄉下應該沒剩多少, 吃過晚飯天色還早, 趁著雨勢變小戴上斗笠去搶收. 沒錯, 我家的水果必須先下手為強才能吃得到. 

久病多年的二姨媽週五早上往生了, 我七月上旬還提了自家芒果去看她, 那時看來精神還不錯, 雖然每次都能認出我來, 但聽表妹講, 其實到了半夜會錯亂而亂喊, 與去看她時似乎能侃侃而談極為不同. 以往去看她有時會說好久沒看到我母親, 我只好無奈地轉頭看向二姨丈; 有時又跟我抱歉說她腳氣不好, 沒能參加我母親的告別式 ... 遺忘真的會讓身邊的人感到氣餒, 但或許對她而言, 忘了比較好.  

最近股市似乎在 AI 帶動下不斷往上升, 我主要的投資標的 0056 也突破 36 元, 可能季配息快到了 (我沒時間查), 買盤持續湧入, 所以我也持續賣出. 過去一年多我都按兵不動, 曾經帳面虧損逾 30%, 雖然持續往下買 (最低買到 23.7 元), 但買太快導致子彈打光, 有更低價時反而無力再進場.  所以熊市時別太急著往下買, 應該慢慢買, 跌越多買越多, 寧可今天沒買到低價, 也不要明天沒買到更低價. 

據多年來觀察與實際交易 0056 經驗, 最近幾年它主要價格帶在 26~36 左右, 只要價格減成本 (均價) 高於年配息我就不參加除息, 往上分批賣出; 買入條件就是現價低於平均成本 (也就是帳面出現虧損時). 我覺得 0056 是非常安全又不用太花時間照顧的標的, 即使套牢也能安心領股息, 然後等彈上來再賣股轉定存 (但不會賣光, 只是減持至 30~50% 而已), 就像現在這時候. 如果年輕時能懂得這樣做, 就不需要花那麼多時間研究又白忙一場了. 人生有更多值得追求的東西, 不需要浪費時間在金錢遊戲, 穩穩賺就好了. 

momo 購買 5TB 2.5 吋硬碟

最近好幾次看到 momo 的 2.5 吋硬碟優惠價 3000 元出頭的廣告, 但都只是記錄下來而已, 一忙就錯過了. 今天午飯後滑手機看到 3790 下安就折 700, 馬上手刀就出鞘買了 : 





用掉 28 元 momo 幣, 實付 3062 元. 明年公務筆電到貨需要用來複製 OA 電腦資料. 

我在 2019 年於建國路買一顆 4TB 的要 2999, 四年後多了 1TB 價格大致不變 :


2023年7月29日 星期六

Swift 5 筆電的 Acer Care Center

我在 2019 年初換了這台 Acer Swift 5 筆電至今已逾四年半, 使用起來非常順手, 平常都插上電源, 只有需要開會前會充到 100%, 電池續航力還可以撐過 4 小時, 維持得還不錯, 主要是它內件 Acer Cer Center 軟體可以設定電池不要充到 100%, 建議是 80% 即可. 

按右下角的方框標誌啟動 AcerCare Center : 




中間的黃框會依序顯示功能表, 等顯示到 "電池" 這一項時按一下黃框 :




切換中間 "電池狀況" 至第二頁 :




將中間 "電池充電限制" 底下的選項按鈕打開 (顯示 On) 即可 :




有充電插座時一直充沒關係 (能充就充, 減少電池循環次數, 但不要經常充到 100%), 但偶而 (例如開會) 要讓電池放電到 20% 再充, 這樣筆電的電池就不會衰退太快. 

2023年7月28日 星期五

惱人的 NumLk 鍵

下午用筆電的瀏覽器搜尋時, 剛輸入 "apple" 卻發現出現的是 "a**3e", 檢查右下角輸入法確定是英文輸入啊? 怎麼會出現這麼奇怪的現象? 




用另一台筆電 (ThinkPad) 查詢 "筆電按 p 出現 *" 找到下面這篇 : 


原來是誤按了 NumLk 鍵, 但我根本沒在用 NumLk 啊! 唯一的罪人便是常跳上我書桌踩我筆電鍵盤的阿咪 !




少在那裏裝無辜, 就是妳! 

露天購買小腿保暖套

最近鼻子過敏又犯了, 這兩三天頻打噴嚏, 可能是體質偏寒, 只要辦公室冷氣稍強, 小腿就會覺得涼颼颼觸發過敏. 前天在露天找到下面這款小腿保暖套, 覺得很實用先買一個試試, 如果好用可在買兩個放家裡與鄉下 :





全家免運 253 元.

好站 : 夜市小霸王教學頻道

前陣子在蝦皮買了小霸王的 ESP32-CAM 的 EasyCam 擴充版套件, 也上過了尤博為此開的線上學習課, 但可惜我一直在忙到現在都沒時間玩. 今天在小霸王的 YT 頻道發現錄影已經上架 :





等我有空就來重看一下錄影邊學邊做. 我很早就買了 ESP32-CAM (第一片就是跟小霸王買的, 尤博的創業歷程很勵志啊), 但慚愧的是我居然一直沒時間玩. 小霸王教學頻道還有許多非常值得一看的物聯網教學影片, 參考 :


好站 : 如何善用 ChatGPT 輔助資料分析與視覺化工作?

前陣子在 1111 人力銀行生成式 AI 系列免費課程中上了彭其捷的 ChatGPT 課, 搜尋網路發現他的部落格內容蠻豐富的, 除了 ChatGPT 外還有資料視覺化方面的文章, 我比較感興趣的是與 Python 相關的部分 (人生苦短, 我只用 Python) :


只要善用 ChatGPT, 程式設計師就不用在一個個小枝節上卡關 (例如忘記正規式怎麼寫或如何在串列迭代中取得索引等等), 快速地完成專案, 我最近用 tkinter 完成了兩個 GUI 程式專案就是靠著 ChatGPT 的幫忙才能在兩個月內迅速結案, 這在以前恐怕要花個至少半年以上. 但如果說 AI 要完全取代軟體工程師恐怕還言之過早, 畢竟 LLM 目前的能耐就只是個博學的接龍者罷了. 

杜蘇芮的颱風假

睽違多年之後昨天高雄終於放到颱風假了 (老闆心裡應該很嘔吧), 但老實說昨天雨勢不大, 風也不強, 這個假放得有點心虛啊! 但其實我一點也不心虛, 因為昨日一整天還是在家上班繼續寫第二個 Tkinter 的 GUI 程式 (CSR 相關), 把最後一個功能項 (單筆增刪與查詢) 寫完, 但還是得上班時實際上線測試看看有無 bug.

今早起床看到外面風雨交加, 吃過早餐後想說今天得開車上班了, 打開手機才發現南高屏早上緊急宣布周五也放颱風假 :




看到這新聞我是一則以喜, 一則以憂啊 (老實說, 喜:憂=6:4), 因為我心裡盤算著今天去公司測試昨天寫完的程式, 沒問題的話就打包上線運轉, 看來要下周一才能結案了. 今天難得的颱風假就用來繼續學習中斷已久的 Streamlit 吧! 

2023年7月25日 星期二

MIT 的汽電混合動力太陽能無人機

今天早上內訓課程請到虎尾科大飛機系老師講無人機, 但我早上為年度體檢無法參加, 故昨日去信給老師詢問是否有錄影, 還好會錄影. 但今早體檢流程迅速, 一個小時就完成了, 所以回到辦公室還能趕上課程. 

老師課堂主要講解虎尾創下 22 小時連續飛行的太陽能無人機設計製作過程, 這種飛機怕風, 最好能飛到平流層飛行較穩定, 過夜飛行與緯度日照時間有正向關係. 末了介紹了長滯空飛行的新方向是汽電混合動力模式, 例如 MIT 曾製作過一架這種無人機, 創下五天連續飛行紀錄 :





不過我查了一下吃汽油或甲醇的航模發動機不便宜, 最便宜也要近 8000 元 :


露天買書 1 本 : Python視窗程式設計與AI遊戲製作

今天在搜尋 tkinter 資料時找到下面這本全華的書 : 



Source : 博客來


由於圖書館都沒收藏, 且 momo 又沒賣權華的書 (很奇怪), 只好網購. 所幸露天有賣二手的才 190 而已 (無光碟), 在高雄可面交 (週三晚上 07:30 在 7-11 新大豐門市) :






看目錄可知此書使用 Tk canvas 畫布, 並非 tkinter 元件, 不過遊戲範例還蠻多的, 且有介紹多執行緒 (遊戲必備). 

此二手書無光碟也無妨, 因為市圖有收藏此書, 只是預約的人蠻多的, 若需要光碟再排隊預約也不遲 :


2023年7月24日 星期一

明儀買書 1 本 : 槓桿ETF投資法

最近在 momo 看到下面這本書覺得很有興趣 :


但可惜只有 79 折, 於是傍晚去河堤健走完順路去明儀買, 我有 VIP 卡 75 折. 其實我查詢市圖發現有進此書, 但是每一本都預約排隊到超過 12 個, 那不就要到明年才輪得到? 乾脆買一本較快. 



好站 : iT 邦幫忙 C 語言基礎教學

今天在整理瀏覽器書籤時發現五月底準備 C 語言授課時找到的 C 語言教材, 我覺得整理得很棒, 不管是入門初學或老手複習都很好用 : 


好站 : Jason Chen's 的部落格

今天在找深度學習 CNN 的資料時發現了 Jason Chen's 的部落格 : 


版主涉獵甚廣, 與我的興趣重疊度大, 例如資料科學, 機器學習, 物聯網等, 而且文章很有深度 (我的則像是雜記簿), 有空要好好來學習一番, 先記下了. 

2023年7月23日 星期日

2023 年第 29 周記事

本周因周一請假幫阿蘭做百日, 只上了四天班感覺一下子就過了. 本周仍是忙, 抽空測試了第二個軟體 (CSR) 發現一些問題, 主要是全刪新增模式執行時間太長, 有空窗期問題, 所以改用第二種執行模式, 即先比對前一日與後一日資料, 相同的 keep, 多出來的新增, 已出局的刪除. 最終採用兩種模式並存, 下周測試看看第二模式會省多少時間. 希望下周就能報完工, 我還有很多事情要做. 

今日午前陽光炙烈, 但午後卻突然下了場大雷雨, 原以為沒辦法去爬山了, 沒想到四點一過就停了, 還出太陽, 但北邊雲層還是密布, 趕緊把握難得的空檔去爬山, 下周有颱風要來, 週四週五應該無法去運動, 要先補起來. 昨日上山時到半山腰突然一輛白色轎車從山下開上來, 越過我時坐在副駕的小女生探出頭來跟我搖手說嗨, 我也搖手回禮, 想說是認識的人嗎? 但幾分鐘後我走到山上廟裡時卻沒看到那輛車子? 我就納悶了, 七月不是還沒到嗎? 

傍晚吃過飯後瞧瞧這天色也不算晚, 還有點餘霞, 就拎個小桶子到後院駁坎上面巡看看有沒有掉下來的芒果, 撿了兩顆, 其中一個壞掉了, 另外一顆黑香則是半邊摔到, 只剩另一邊還好的. 舉頭望去, 那 1000 多顆芒果如今大概剩下 5 顆左右, 今年的芒果已經到尾聲了. 但我感覺怎麼好像沒採收到這麼多啊. 接下來就是換龍眼上場了, 大概還要兩周, 這次我要先下手為強惹. 

這周由於晚上也沒甚麼線上課, 加上軟體專案已接近收尾, 就耍廢看了兩部韓劇 : 一是南宮珉與李清娥, 金雪炫主演的 "日與夜", 描寫軍政府時期的掌權者與財團勾結在離島設立白夜研究所與白夜村, 利用孤兒院院童當人體實驗的白老鼠, 企圖研究能讓人不老永生的藥劑, 南宮珉 (飾都正宇警正) 與李清娥 (飾潔米萊頓) 就是白夜研究所主要研究員之一的趙賢熙為了實驗, 自己生的一對龍鳳胎, 於母胎時期開始施打藥劑, 令其基因突變, 出生後腦細胞會演化出超能力, 透過抽取血清進行永生研究. 其中南宮珉是所有院童中最優秀的, 其智商迴異常人 (但後遺症是會有諸如解離性人格問題), 小小年紀就能寫出永生藥物的化學式, 但他早已發覺白葉村的怪異, 於是塗掉了一部份化學式, 並在目睹許多院童因實驗而死亡後, 決心用他的超能力 - 清醒夢讓村裡的工作人員在現實夢中走入滅亡之路, 兩兄妹與少數院童則在滅村慘案後逃出, 南宮珉後來成為警察, 李清娥則被駐韓美軍收養, 成為 FBI 探員. 由於陸續出現的預告殺人案受害者都是與白夜財團有關, 警方請美國 FBI 派顧問協助調查, 這個顧問就是白葉村倖存者之一的李清娥. 看完後的心得是, 使命感如果用錯地方會是天大的災難. 另外, 不知道是否是上帝會特地開一扇窗, 有腦神經相關問題的人似乎都有一種常人沒有的能力, 譬如智商高達 150 以上之類的. 

另一部是金泰梨主演的 "惡鬼", 這部還在追劇中, 已播出第 10 集. 

momo 購買行軍床

最近暑熱難耐, 周末回鄉下時因臥室西曬, 晚上外面很清涼, 但進入臥室卻熱烘烘, 吹電扇也是吹熱風, 所以都在客廳長椅上睡. 其實頂樓陽台有風更涼, 很早就想買個行軍床到頂樓陽台睡, 剛好今天 看到 momo 廣告就買了 :






扣掉 momo 幣 51 元, 實付 691 元. 

另一款也不錯, 都是 7/23 日 74 折 :


2023年7月22日 星期六

開啟 Win10 的 Telnet 客戶端

今天在查詢 Telnet 伺服器時找到下面這篇, 原來 Windows 有內建 Telnet 客戶端 : 


我照文章指引, 到控制台的程式與功能, 按左上方的 "開啟或關閉 Window 功能", 勾選 "Telnet 用戶端" :




然後用系統管理員身分開啟命令提示字元視窗, 輸入下列指令即可 :

dism /online /Enable-Feature /FeatureName:TelnetClient




C:\WINDOWS\system32>dism /online /Enable-Feature /FeatureName:TelnetClient        

部署映像服務與管理工具
版本: 10.0.19041.844

映像版本: 10.0.19045.3086

啟用功能
[==========================100.0%==========================]
操作順利完成。

C:\WINDOWS\system32>

可惜現在 PTT 已關閉無加密的 Telnet 連線, 所以我找到下面這個 Telnet 伺服器來測試 :

C:\WINDOWS\system32>telnet 123.45.67.89 1521

市圖大還書

今天整理書架將借來甚久卻無暇閱讀的書全部拿去還掉 : 
  1. 大指導力 :宋王朝領導菁英結構群透視<<宋名臣言行錄>>白話版
  2. 虛擬實境的商業化應用 : 遠比現實世界自由的VR世界
  3. 機器人會變成人嗎? : 33則最令現代人焦慮的邏輯議題
  4. 完全圖解物聯網 : 實戰.案例.獲利模式 從技術到商機.從感測器到系統建構的數位轉型指南
  5. 進擊的行為科學 : 更適配的決策, 來自更深刻的人性測量
  6. C++教學手册
  7. 2030科技趨勢全解讀 : 元宇宙.AI.區塊鏈.雲端.大數據.5G.物聯網, 七大最新科技一本就搞懂!
  8. 經絡導引 : 融合中醫、易筋經、八段錦、六字訣、吐納、推拿的伸展放鬆術
  9. 在家CEO!賺進後半輩子從家開始 : 30、40、50世代, 找出陪自己到老的工作與收入
  10. 大人的周末創業 : 讓經驗、人脈、興趣變現金的未來獲利術
  11. 20個字的精準文案 : 「紙一張整理術」再進化, 三表格完成最強工作革命 /
  12. 一日三餐蒸出健康來 : 四季調養、五臟保健、對症治病, 吃出不生病的體質
因為要看的書實在太多了, 只能將有限精力花在更有價值的東西上, 這些閒書就先記下來, 反正以後要看也借得到. 

2023年7月20日 星期四

高科大還書 2 本 (Django & 機器學習)

 今天中午午休去母校取預約書 (WordPress), 拿下面兩本去換 :
No.1 我已借到了第二版 Django 3 的新書 (Django 我從 1 版學到現在居然還沒完結), 舊版可以還了. No.2 是最近才發現我早就有買 (買書買到忘記曾經買過, 笑鼠, 還好沒重複買). 只要去母校還書, 我都會到附近的鄧園吃雞腿飯 (真的好吃). 暑假了, 這裡學生還是很多, 以前我也是在這條建工路上走動, 但那已是四十年前了.  

2023年7月19日 星期三

參加台中 Python 社群的 Pynecone (reflex) 分享會

上週三參加了台中 Python 社群的 Pynecone (reflex) 分享會, 參考 :


我對這個純 Python 全端開發套件感到非常驚艷, 它跟 Streamlit 的用途非常類似, 就是不需要去涉獵 HTML & CSS & Javascript, 單靠 Python 就能搞定前後端, 據說社群關注度頗高, 目前還在 beta 快速演進時期. 目前我還沒時間去玩, 先記下來持續追蹤.

2023年7月18日 星期二

阿蘭百日

昨日 7/17 (一) 請假一天為吾妹做百日, 我週六先回鄉下準備, 檢查百日錢包有兩份無誤, 並向退伍軍人訂了紅粄與發粄各兩包 (每包 6 個, 紅粄一個15 元, 發粄一個 18 元). 鄰居妹妹在告別式那天交給我一份做百日要準備的用品單 :

"祭品兩份 (葷牲禮或素水果), 紅粄與發粄 (各兩包), 花束一對, 蠟燭一對, 九金銀紙 (各一條), 壽金 (一仟), 香 (適量)"

祭品採用五樣水果 (蘋果, 水梨, 奇異果, 木瓜, 橘子, 均單數個) 與素三牲, 外加米果與餅乾, 我都在全聯採辦妥當. 姊姊週日晚上到高雄, 我晚上出高雄去載大家回來. 

周一早上到觀音廟後, 先將祭品擺放好, 同一桌分左右兩邊 (左邊是阿蘭的, 另一邊是王官的), 香爐也是兩個 (黃的是亡者, 紅的是王官), 錢包倚在香爐前. 祭拜程序是先舉香向地藏菩薩稟告今日為吾妹做百日事宜, 然後進去塔內向阿蘭說明今日為其做百日, 請其靈到外面供桌享用祭祀. 然後出來在供桌前舉香拜, 先拿王官錢包念誦上面的祭文, 舉香拜後插王官的紅香爐, 接著拿阿蘭的錢包念誦其上之祭文, 舉香拜後插香爐即可. 二十分鐘後先將王官錢包與壽金拿去金爐燒化, 然後才將阿蘭的錢包與九金銀紙等拿去塔右側廣場燒化 (不可在金爐), 這樣做百日的祭祀便完成了.  

接下來是農曆 12 月 25 的分年祭祀, 要準備的用品與百日相同, 但多了芹菜與蒜苗各一株這項 (都要含根莖葉, 根部要纏上紅紙). 

來年大利東西, 便可看日子添金, 將阿蘭骨灰罈移到家族墓園. 

2023年7月16日 星期日

2023 年第 28 周記事

這一周又是忙碌的一周, 因為下周有服務要上線, 臨時發現原來的規劃在末端沒作用, 只好採取修補措施, 忙到原本周一計畫要去匯款給信望愛硬是拖到周五才去, 這筆錢是社會局每個月匯入阿蘭帳戶的錢, 她小時候曾被信望愛的王牧師收養, 所以我想那就以她的名義捐給信望愛吧  (總計 8 萬元).  


由於白天要忙服務上線, 只有晚上有時間寫第二個自動化程式, 我週二就寫好了 (有上週完工的維運軟體當範本), 加上 ChatGPT 顧問的幫忙, 這次完工時間更快), 但卻沒時間測試. 今天花了一整個下午時間, 把操作說明部分都搞定了, 也因此發現上回以為 tkhtmlview 套件無法讀取 UTF-8 編碼的網頁問題, 其實可以用 codecs 套件解決. 用 tkinter 連續寫了兩個視窗軟體後, 終於對 tkinter 有較深入的掌握了, 實戰才是真正能學到東西的途徑. 

家樂福買的冰棒模子買來兩周一直沒時間動手, 今天下午終於有空做芒果冰, 先將切好的芒果肉放進果汁機, 加入適量牛奶打成泥, 倒進冰棒模子與紙碗中後放冰庫 : 





2023年7月13日 星期四

市圖還書 1 本

昨天市圖還書 1 本 : 

Source : 博客來

此書我早已看完, 但還沒時間做筆記就被預約得先還. 此書一套目前共兩本, 第一集主要講明朝前期皇帝 (即朱元璋與其兒子孫子) 父子兄弟之間的恩怨情仇鬥爭史, 當然也少不了要著墨建文帝朱允炆生死下落之謎. 第二集則是聚焦於后宮女人的爭奇鬥艷與閹人如何左右政局 : 



Source : 博客來


第二集也看完了, 有空再來整理筆記. 我特愛看明史, 夠血腥, 夠奇葩. 

2023年7月9日 星期日

Python 學習筆記 : 在 Tkinter 中使用執行緒執行耗時函式的方法

上週完成維運自動化軟體後馬上開工寫第二個自動化軟體 (與 CSR 有關), 因為近來有關部門提交的數據量大幅增長, 我以前用網頁技術撰寫的舊版軟體因為須與系統在 timing 上同步, 導致執行時間過長, 因此決定改用 Python 來改寫. 正因為要執行的數據較多, 因此想在按下自動執行按鈕後用彈出視窗來顯示一個進度條與例如 '正在執行資料更新作業, 請稍後' 這樣的提示, 避免因為執行中無回應而讓操作者以為程式當掉了. 

在 Tkinter 中彈出視窗元件是 TopView, 它跟 root 視窗一樣可以放置各種元件, 這裡則只是要放一個進度條 ProgressBar 與一個用來顯示執行狀態的 Label 元件而已 (本來想將提示字串直接顯示在進度條上面, 但查詢 ProgressBar 物件的方法發現並無此功能, 所以進度提示必須放在 Label 上). 最重要的是, 因為執行資料更新作業與顯示執行進度必須同時進行, 因此進度條視窗必須利用另一個執行緒來跑, 否則兩個都在主執行緒的話, 進度條會在漫長的執行作業結束後才顯示, 這樣就失去意義了. 

以下是簡化過後的範例, 主視窗 root 上只有一個開始執行的按鈕, 按下去會觸發執行一個 start_running_task() 函式, 此函式會一個彈出視窗來顯示進度條, 然後建立一個執行緒來執行傳進來的那個無法確定多久可以執行完畢的函式, 彈出視窗會定期 (這裡設定每 100 ms) 去檢查該作業執行緒是否還存在 (工作完成執行緒就會自動結束), 如果執行緒已結束就關閉顯示進度條的彈出視窗, 範例程式碼如下 : 


測試 1 : 使用執行緒執行耗時函式 [看原始碼]

import tkinter as tk
from tkinter import ttk
from tkinter import messagebox as msgbox
import time
import threading

def start_running_task(task, msg):
    def check_thread():   # 檢查執行緒
        if thread.is_alive():   # 執行緒還沒結束 : 繼續每 100 ms 檢查一次
            progress_win.after(100, check_thread) # 每 100 ms 檢查執行緒 
        else:  # 若執行緒已結束關閉進度條視窗            
            progress_win.destroy()
            msgbox.showinfo('通知訊息', '執行作業已完成')
    # 建立 TopLevel 彈出視窗
    progress_win=tk.Toplevel(root)  
    progress_win.title('處理進度')
    progress_win.update_idletasks()  # 強制更新視窗更新, 處理所有事件
    progress_win.geometry('400x140')  # 將彈出視窗左上角拉到指定座標並設定大小
    progressbar=ttk.Progressbar(progress_win, mode='indeterminate', length=250)
    progressbar.pack(padx=3, pady=30)
    progressbar.start(5)   # 起始進度條並設定速度 (值越小跑越快)
    label=ttk.Label(progress_win)  # 顯示提示詞用
    label.config(text=msg, font=('Helvetica', 10, 'bold'))  # 設定字型大小
    label.pack(padx=3, pady=3)    
    thread=threading.Thread(target=task)  # 建立執行緒
    thread.start()    # 啟始執行緒
    progress_win.after(100, check_thread)  # 每 100 ms 檢查執行緒是否還存活    

def task():
    time.sleep(10)   # 模擬漫長的執行時間

root=tk.Tk()
root.title('執行緒測試')
root.geometry('600x400')
start_button=ttk.Button(root, text='開始執行', command=lambda: \
                        start_running_task(task, '執行中請稍候 ...'))
start_button.pack()
root.mainloop()

這裡在長時間作業函式 task() 中我使用 time.sleep() 來模擬那漫長的資料更新作業 (實際的系統是透過 Telnet 連線後向遠端主機連續丟出大量資料更新指令). 當按下按鈕時 command 參數的 lambda 會呼叫 start_running_task() 並傳入長時間作業函式 task 與要顯示在彈出視窗的提示字串,  start_running_task() 函式會建立一個執行緒來執行此作業函式 task(), 並用 after() 方法每 100 毫秒呼叫回呼函式 check_thread() 來檢查作業函式 task() 是否執行結束, 是的話就關閉彈出視窗, 否則繼續每 100 毫秒檢查一次直到 task() 結束, 執行緒終止才關掉彈出視窗, 結果如下 : 


 


其實不是每個作業函式都要丟給 start_running_task(), 只有作業時間較長 (通常使用者耐性大約 5~10 秒) 的作業才需要, 一般 3~5 秒就完成的函式進度條才閃一下就結束了, 丟給 start_running_task() 沒意思. 

如果要讓顯示進度條的彈出視窗顯示在整個螢幕的正中央, 可以加入設定彈出視窗 geometry 的程式碼, 如下面黃色背景的部分 :


測試 2 : 使用執行緒執行耗時函式 (彈出視窗置中) [看原始碼]

import tkinter as tk
from tkinter import ttk
from tkinter import messagebox as msgbox
import time
import threading

def start_running_task(task, msg):
    def check_thread():   # 檢查執行緒
        if thread.is_alive():   # 執行緒還沒結束 : 繼續每 100 ms 檢查一次
            progress_win.after(100, check_thread) # 每 100 ms 檢查執行緒 
        else:  # 若執行緒已結束關閉進度條視窗            
            progress_win.destroy()
            msgbox.showinfo('通知訊息', '執行作業已完成')
    # 建立 TopLevel 彈出視窗
    progress_win=tk.Toplevel(root)  
    progress_win.title('處理進度')
    progress_win.update_idletasks()  # 強制更新視窗更新, 處理所有事件
    # 設定彈出視窗左上角座標與大小始其置中
    width=400   # 彈出視窗寬度
    height=140  # 彈出視窗高度    
    screen_width=progress_win.winfo_screenwidth()    # 取得彈出視窗寬度
    screen_height=progress_win.winfo_screenheight()  # 取得彈出視窗高度
    x=(screen_width // 2) - (width // 2)    # 彈出視窗的左上角 x 座標
    y=(screen_height // 2) - (height // 2)  # 彈出視窗的左上角 y 座標  
    geometry=f'{width}x{height}+{x}+{y}'    # 設定視窗左上角座標與大小
    progress_win.geometry(geometry)  # 將彈出視窗左上角拉到指定座標並設定大小
    progressbar=ttk.Progressbar(progress_win, mode='indeterminate', length=250)
    progressbar.pack(padx=3, pady=30)
    progressbar.start(5)   # 起始進度條並設定速度 (值越小跑越快)
    label=ttk.Label(progress_win)  # 顯示提示詞用
    label.config(text=msg, font=('Helvetica', 10, 'bold'))  # 設定字型大小
    label.pack(padx=3, pady=3)    
    thread=threading.Thread(target=task)  # 建立執行緒
    thread.start()    # 啟始執行緒
    progress_win.after(100, check_thread)  # 每 100 ms 檢查執行緒是否還存活    

def task():
    time.sleep(10)   # 模擬漫長的執行時間

root=tk.Tk()
root.title('執行緒測試')
root.geometry('600x400')
start_button=ttk.Button(root, text='開始執行', command=lambda: \
                        start_running_task(task, '執行中請稍候 ...'))
start_button.pack()
root.mainloop()

這樣彈出視窗就會出現在全螢幕的正中央了. 我查書與爬文都沒找到滿意合用的, 以上的做法是我不斷調整 prompt 詢問 ChatGPT 好多次 (它的回答不一定有效) 摸索測試出來的可行架構, 特地記下來以便往後的專案參考. 如何善用 AI 加速軟體專案開發很值得探討. 


2023-07-10 補充 :

如果要在執行作業完成時顯示所耗費的時間, 可以在 start_running_task() 裡面計算起訖時間, 作法如下面範例所示 : 


測試 3 : 使用執行緒執行耗時函式 (顯示執行時間) [看原始碼]

import tkinter as tk
from tkinter import ttk
from tkinter import messagebox as msgbox
import time
import threading

def start_running_task(task, msg):
    def check_thread():   # 檢查執行緒
        nonlocal start_time   # 參照外部變數
        if thread.is_alive():   # 執行緒還沒結束 : 繼續每 100 ms 檢查一次
            progress_win.after(100, check_thread) # 每 100 ms 檢查執行緒 
        else:  # 若執行緒已結束關閉進度條視窗            
            progress_win.destroy()
            end_time=time.time()     # 結束時間
            elapsed=round(end_time - start_time, 2)   # 計算耗時
            msgbox.showinfo('通知訊息', f'執行作業已完成, 耗時 {elapsed} 秒')
    start_time=time.time()  # 開始時間 (計算執行時間用)
    # 建立 TopLevel 彈出視窗
    progress_win=tk.Toplevel(root)  
    progress_win.title('處理進度')
    progress_win.update_idletasks()  # 強制更新視窗更新, 處理所有事件
    progress_win.geometry('400x140')  # 將彈出視窗左上角拉到指定座標並設定大小
    progressbar=ttk.Progressbar(progress_win, mode='indeterminate', length=250)
    progressbar.pack(padx=3, pady=30)
    progressbar.start(5)   # 起始進度條並設定速度 (值越小跑越快)
    label=ttk.Label(progress_win)  # 顯示提示詞用
    label.config(text=msg, font=('Helvetica', 10, 'bold'))  # 設定字型大小
    label.pack(padx=3, pady=3)    
    thread=threading.Thread(target=task)  # 建立執行緒執行 task() 函式
    thread.start()    # 啟始執行緒
    progress_win.after(100, check_thread)  # 每 100 ms 檢查執行緒是否還存活    

def task():
    time.sleep(10)   # 模擬漫長的執行時間

root=tk.Tk()
root.title('執行緒測試')
root.geometry('600x400')
start_button=ttk.Button(root, text='開始執行', command=lambda: \
                        start_running_task(task, '執行中請稍候 ...'))
start_button.pack()
root.mainloop()

這裡因為檢查執行緒的關係使用了巢狀函式, 內部函式若要參照外部變數 start_time, 必須於內部函式中宣告為 nonlocal, 執行結果如下 : 




如果要讓 start_running_task() 可以傳入參數控制彈出視窗中 Label 顯示的訊息, 以及執行緒結束對話框內顯示的訊息, 改寫如下 : 


測試 4 : 使用執行緒執行耗時函式 (以參數控制顯示訊息) [看原始碼]

import tkinter as tk
from tkinter import ttk
from tkinter import messagebox as msgbox
import time
import threading

def start_running_task(task, msg_start='作業執行中 ...', msg_end='已執行完成'):
    def check_thread():   # 檢查執行緒
        nonlocal start_time   # 參照外部變數
        if thread.is_alive():   # 執行緒還沒結束 : 繼續每 100 ms 檢查一次
            progress_win.after(100, check_thread) # 每 100 ms 檢查執行緒 
        else:  # 若執行緒已結束關閉進度條視窗            
            progress_win.destroy()
            end_time=time.time()
            elapsed=round(end_time - start_time, 2)   # 計算耗時
            msgbox.showinfo('通知訊息', f'{msg_end}, 耗時 {elapsed} 秒')
    start_time=time.time()  # 計算執行時間用
    # 建立 TopLevel 彈出視窗
    progress_win=tk.Toplevel(root)  
    progress_win.title('處理進度')
    progress_win.update_idletasks()  # 強制更新視窗更新, 處理所有事件
    progress_win.geometry('400x140')  # 將彈出視窗左上角拉到指定座標並設定大小
    progressbar=ttk.Progressbar(progress_win, mode='indeterminate', length=250)
    progressbar.pack(padx=3, pady=30)
    progressbar.start(5)   # 起始進度條並設定速度 (值越小跑越快)
    label=ttk.Label(progress_win)  # 顯示提示詞用
    label.config(text=msg_start, font=('Helvetica', 10, 'bold'))  # 設定字型大小
    label.pack(padx=3, pady=3)    
    thread=threading.Thread(target=task)  # 建立執行緒
    thread.start()    # 啟始執行緒
    progress_win.after(100, check_thread)  # 每 100 ms 檢查執行緒是否還存活    

def task():
    time.sleep(10)   # 模擬漫長的執行時間

root=tk.Tk()
root.title('執行緒測試')
root.geometry('600x400')
start_button=ttk.Button(root, text='開始執行', command=lambda: \
                        start_running_task(task))
start_button.pack()
root.mainloop()

此例修改了 start_running_task() 的參數結構, 改為可傳入 msg_start 與 msg_end 兩個參數, 兩者皆有預設值, 如要更改可在呼叫 start_running_task() 時傳入參數, 例如 : 

start_running_task(task, '執行中請稍候 ...', '工作以執行完成')

下面是以預設值執行的結果 : 





如果執行結束時需要從 task() 函式取得資訊 (例如執行的指令失敗的個數) 顯示在結束的對話框要怎麼做? 這種情況要修改上面的程式碼, 添加一個內部中介函式例如 run_task() 來取得 task() 的傳回值, 並且將檢查執行緒的定時器函式 after() 移到 run_task() 裡面來並增加一個傳回值參數, 同時 check_thread() 要添加一個參數來取得 run_task() 傳回來的結果, 透過這個傳回值來製作完成對話框要顯示的資訊, 這樣也省了上面範例的 end_msg 參數了, 程式如下 : 


測試 5 : 使用執行緒執行耗時函式 (從作業函式取得傳回值) [看原始碼]

import tkinter as tk
from tkinter import ttk
from tkinter import messagebox as msgbox
import time
import threading

def start_running_task(task, msg='作業執行中 ...'):
    def check_thread(result):   # 檢查執行緒, result 為 task() 之傳回值
        nonlocal start_time     # 參照外部變數
        if thread.is_alive():   # 執行緒還沒結束 : 繼續每 100 ms 檢查一次
            progress_win.after(100, check_thread) # 每 100 ms 檢查執行緒 
        else:  # 若執行緒已結束關閉進度條視窗            
            progress_win.destroy()
            end_time=time.time()
            elapsed=round(end_time - start_time, 2)   # 計算耗時
            msgbox.showinfo('通知訊息', f'{result}\n耗時 {elapsed} 秒')
    def run_task():   # 中介函式
        result=task()     # 取得作業函式傳回值
        progress_win.after(100, check_thread, result)  # 每 100 ms 檢查執行緒是否還存活

    start_time=time.time()  # 計算執行時間用
    # 建立 TopLevel 彈出視窗
    progress_win=tk.Toplevel(root)  
    progress_win.title('處理進度')
    progress_win.update_idletasks()  # 強制更新視窗更新, 處理所有事件
    progress_win.geometry('400x140')  # 將彈出視窗左上角拉到指定座標並設定大小
    progressbar=ttk.Progressbar(progress_win, mode='indeterminate', length=250)
    progressbar.pack(padx=3, pady=30)
    progressbar.start(5)   # 起始進度條並設定速度 (值越小跑越快)
    label=ttk.Label(progress_win)  # 顯示提示詞用
    label.config(text=msg, font=('Helvetica', 10, 'bold'))  # 設定字型大小
    label.pack(padx=3, pady=3)    
    thread=threading.Thread(target=run_task)  # 建立執行緒執行中介函式 run_task()
    thread.start()    # 啟始執行緒

def job():
    time.sleep(5)   # 模擬漫長的執行時間
    error=2
    return '執行作業已完成, 錯誤數=' + str(error)

root=tk.Tk()
root.title('執行緒測試')
root.geometry('600x400')
start_button=ttk.Button(root, text='開始執行', command=lambda: \
                        start_running_task(job))
start_button.pack()
root.mainloop()

注意, 此例為了避免混淆, 以為內部函式 run_task() 呼叫的 task() 是直接呼叫外部函式 task(), 特地將要執行的漫長作業從 task() 改成 job(), 在按下執行按鈕時呼叫 start_running_task(job) 將 job 函式傳給 start_running_task(), 所以 run_task() 呼叫的 task() 其實就是外部的 job(), 結果如下 :





最後這個例子才是最終用在我的軟體專案中的做法. 

2023 年第 27 周記事

本周仍然忙於為維運自動化軟體改版, 清除幾個使用中發現的 bug (例如 Text 元件選取複製一定要放在 try except 中捕捉使用者未選取之例外). 真正著手做軟體專案才了解, 主體功能可能三兩下就搞定, 但添加功能與除蟲卻花了更多時間. 打鐵趁熱, 同時也開展另一個軟體專案, 寫完這兩個軟體, 未來八年的職場生涯就輕鬆很多了, 以往要花很多時間準備的人工作業, 現在一鍵 OK.  

這一周同時上了四堂全日內訓課程 (改程式與做資料都趁休息時間與犧牲午休),  包括兩堂 scikit-learn 與兩堂 TensorFlow, 下周 scikit-learn 還有兩堂, 這些其實去年都上過不同老師的課, 今年當作複習, 但也從中學到許多新的知識. 雖然胡老師在 TensotFlow 課中強調 PyTorch 的優越性, 但有一句話蠻重要的, 就是大咖神人這兩個都熟. 

今天收到大帥 Line 訊息, 說她太太月初回廈門, 今早幫貓咪洗澡竟然貓咪休克往生, 令我想起家中的阿咪與阿萬兩三年前水某與菁菁有幫牠們洗澡, 結果像是上刑場一樣恐懼得近乎歇斯底里, 阿咪更是濕漉漉地脫離掌握, 跳上乾濕分離門的橫桿上面, 嚇得發出類似牛的哞哞叫聲, 貓似乎天生有恐水症啊! 

自從四月缺水我暫停去游泳, 又沒有去河堤快走, 停止運動兩個月體重又回升到 70 公斤, 本周除周三公休外, 每日下班又恢復去河堤快走, 等暑假結束九月再恢復去游泳, 要不然我之前以便宜價購買的游泳券到 2050 年都游不完. 

週四水某在小七買一送一買了 10 罐檸檬與百香果愛玉凍非常好吃, 愛玉採用桃源鄉原民部落的農產, 我超喜歡愛玉的 :




平均每罐才 10 元真的超划算. 

2023年7月6日 星期四

Python 學習筆記 : 使用 Tkinter 的剪貼簿實作複製與貼上功能

Tkinter 的 Text 元件通常用來輸入或編輯多行文字, 如果要複製部分或全部內容, 可以用滑鼠選取後按 CTRL+C 或按滑鼠右鍵選複製, 這樣便會被複製到作業系統的剪貼簿裡, 方便於其他軟體中貼上. 這種透過剪貼簿複製貼上的功能也可以用 Python Tkinter 程式碼控制. 視窗頂層物件 (即呼叫 tk.Tk() 建立之物件) 可利用  clipboard_append() 與 clipboard_get() 方法存取作業系統的剪貼簿以便實作複製與貼上功能.  

在下面的簡化範例中, 視窗中放置了一個 Text 元件, 以及四個按鈕, 分別是貼上, 清除, 選取複製, 與全部複製, 按下複製鈕會將選取內容存入剪貼簿, 按下貼上則是從剪貼簿取得暫存之內容 :

import tkinter as tk
from tkinter import ttk
from tkinter import messagebox as msgbox

root=tk.Tk()  
root.title('剪貼簿複製貼上測試') 
root.geometry('500x300') 

def paste():  # 從剪貼簿貼到編輯區
    editor_text.delete(1.0, "end")   # 清除編輯區內容
    editor_text.insert(tk.INSERT, root.clipboard_get())  # 插入內容

def clear():  # 清除編輯區
    editor_text.delete(1.0, "end")   # 清除編輯區

def copy_all():
    selection=editor_text.get(1.0, tk.END) # 取得 Text 全部內容
    root.clipboard_clear()  # 清除剪貼簿
    root.clipboard_append(selection)  # 複製到剪貼簿
    msgbox.showinfo('通知訊息', '編輯區內容已全部複製到剪貼簿')

def copy_select():
    try: # 未選取會出現例外
        selection=editor_text.get(tk.SEL_FIRST, tk.SEL_LAST) # 取得被選取內容
        root.clipboard_clear()  # 清除剪貼簿
        root.clipboard_append(selection)  # 複製到剪貼簿
        msgbox.showinfo('通知訊息', '選取內容已複製到剪貼簿')
    except Exception as e:
        msgbox.showinfo('通知訊息', '未選取內容') 

editor_text=tk.Text(root, width=70, height=12)
editor_text.pack(fill=tk.X, expand=True)
paste_btn=ttk.Button(root, text='貼上', command=paste)
paste_btn.pack(side=tk.LEFT)
clear_btn=ttk.Button(root, text='清除', command=clear)
clear_btn.pack(side=tk.LEFT)
copy_select_btn=ttk.Button(root, text='選取複製', command=copy_select)
copy_select_btn.pack(side=tk.LEFT)
copy_all_btn=ttk.Button(root, text='全部複製', command=copy_all)
copy_all_btn.pack(side=tk.LEFT)

root.mainloop()

程式內容摘要說明如下 : 
  • 呼叫視窗物件的 clipboard_append(selection) 方法可將選取內容複製到剪貼簿
  • 呼叫視窗物件的 clipboard_get() 方法可取得剪貼簿內容
  • 呼叫視窗物件的 clipboard_clear() 方法可清除剪貼簿
  • 呼叫 Text 物件的 delete(1.0, "end") 方法可刪除 Text 物件全部內容
  • 呼叫 Text 物件的 get(tk.SEL_FIRST, tk.SEL_LAST) 方法可取得被選取內容, 但未選取時會出現 _tkinter.TclError: text doesn't contain any characters tagged with "sel" 例外, 故須放在 try~except 中捕捉例外. 
結果如下 : 





Python 學習筆記 : Tkinter 的 Entry 點一下清除預設內容的方法

晚上得空繼續來記錄 Tkinter GUI 軟體開發的小技巧, 本篇是常見的一種 Entry 元件的功能, 即點擊Entry 輸入框時自動消除裡面的預設值或輸入提示語. 下面以簡化的登入畫面為例說明, 使用 pack 排版依序將帳號與密碼兩個 Entry 輸入框與登入按鈕放入視窗中.

import tkinter as tk
from tkinter import ttk

root=tk.Tk()  
root.title('點擊 Entry 清除內容測試') 
root.geometry('300x150') 

def clear_account_entry(event):   # 清除帳號框內容
    account_entry.delete(0, tk.END)    # 刪除開頭與結尾索引之間的 (全部) 內容
    
def clear_password_entry(event):  # 清除密碼框內容
    password_entry.delete(0, tk.END)   # 刪除開頭與結尾索引之間的 (全部) 內容
    
def login():
    pass
        
account=tk.StringVar()
account_entry=tk.Entry(root, textvariable=account)
account_entry.insert(0, '請輸入帳號')
account_entry.bind('<Button-1>', clear_account_entry)
account_entry.pack()
password=tk.StringVar()
password_entry=tk.Entry(root, textvariable=password)
password_entry.insert(0, '請輸入密碼')
password_entry.bind('<Button-1>', clear_password_entry)
password_entry.pack()
login_btn=ttk.Button(root, text='登入', command=login)
login_btn.pack()
root.mainloop()

此處兩個 Entry 元件都呼叫 bind() 方法監聽滑鼠左鍵的按一下鈕 (Button-1) 事件, 並綁定事件處理函式. 在事件處理函式中, 呼叫 Entry 物件的 delete(0, tk.END) 方法即可刪除 Entry 內預設值或先前輸入的內容, 結果如下 : 




點擊 Entry 輸入框時預設值 "請輸入 ..." 就會自動清空, 方便我們直接輸入內容 :




高科大還書 3 本

 因有三本預約書到館, 中午午休時拿下面三本去換 :
後備犧牲書目 :
  1. Python程式設計入門與運算思維
  2. 深度學習的16堂課 : CNN.RNN.GAN.DQN.DRL 看得懂、學得會、做得出
  3. Raspberry Pi實例應用集
已經暑假了, 學校停車棚還是很多汽機車, 詢問館員才知有人要暑修, 打工, .....

Python 學習筆記 : 如何在 tkinter 的 Text 元件中搜尋字串並以高亮度顯示

這也是最近用 Tkinter 寫維運自動化軟體的總結經驗之一, 在此軟體的記錄檔管理功能中, 可以讓使用者輸入想要搜尋的字串, 然後在文字區域元件 Text 中用高亮度來標示該字串於文本中出現的地方. 以下用簡化的範例來說明作法.

下面的範例程式中, 使用簡單的 pack 排版方式放置一個 Entry 輸入框, Button 按鈕, 以及 Text 多行文字輸入框, 當按下按鈕時呼叫 search_text() 函式於 Text 元件的內容中做全文搜索, 利用索引逐步推進法找尋 Entry 輸入框中指定之字串直到文本末端, 當找到文本時就設定為高亮度顯示, 程式如下 :

import tkinter as tk
from tkinter import ttk

root=tk.Tk()  
root.title('搜尋文本高亮度顯示測試') 
root.geometry('400x300') 

def search_text():
    search_what=search_what_var.get()  # 取得搜尋標的
    text.tag_remove('highlight', '1.0', tk.END)  # 移除全部高亮度顯示標籤
    start='1.0'  # 預設起始索引為文本開頭
    while True:  # 搜尋全部文本直到末端
        start=text.search(search_what, start, stopindex=tk.END)
        if not start:  # 起始索引為 0 : 已到末端結束搜尋
            break
        end=f'{start}+{len(search_what)}c'  # 末端字串
        text.tag_add('highlight', start, end)  # 將搜尋到的字串設為高亮度
        start=end  # 後軍當前軍, 移動開始索引到目前標的尾端
        
search_what_var=tk.StringVar()   # 綁定 Entry 元件的字串變數
search_entry=tk.Entry(root, textvariable=search_what_var) 
search_entry.pack()
search_btn=ttk.Button(root, text='搜尋', command=search_text)
search_btn.pack()
text=tk.Text(root)
text.pack(fill=tk.BOTH, expand=True)
text.tag_configure('highlight', background='yellow')  # 設定 Text 高亮度為黃色
root.mainloop()

結果如下 :




處理函式 search_text() 並非我原創, 而是透過詢問 ChatGPT 改寫而來. ChatGPT 真是程式員的好幫手啊! 因為以後會重複用到, 所以特別寫個簡化範例來記錄作法. 

2023年7月5日 星期三

Python 學習筆記 : 如何替 tkinter 的元件加上工具提示 (tooltip) 文字

這周終於把維運自動化軟體寫完, 從中累積了許多 tkinter 小技巧, 本篇紀錄替元件加上 tooltip 的方法. 以下用簡化的範例來說明如何用 IDLE 編輯器的 idlelib.tooltip.Hovertip 類別來為 tkinter 的元件加上 tooltip (工具提示), 此套件為 Python 內件不需安裝, 使用之前需要用 import 匯入 :

from idlelib.tooltip import Hovertip   

加 tooltip 的語法如下 (此函式有傳回值, 但通常用不到) :

Hovertip(物件變數, '提示詞')   

例如 : 

import tkinter as tk
from tkinter import ttk
from idlelib.tooltip import Hovertip

root=tk.Tk()  
root.title('Hovertip 測試') 
root.geometry('400x300') 

entry=tk.Entry(root)  
Hovertip(entry, '這是輸入框')
entry.pack()  
tk_btn=tk.Button(root, text='OK') 
Hovertip(tk_btn, '這是 tk 按鈕')
tk_btn.pack() 
ttk_btn=ttk.Button(root, text='OK') 
Hovertip(ttk_btn, '這是 ttk 按鈕')
ttk_btn.pack()
radio=ttk.Radiobutton(root, text="男", value="M")
Hovertip(radio, '這是 radio 圓鈕')
radio.pack()
checkbutton=ttk.Checkbutton(root, text='日語')
Hovertip(checkbutton, '這是 checkbutton 方塊')
checkbutton.pack()
text=tk.Text(root) 
Hovertip(text, '這是 Text 文字區域')
text.pack()

root.mainloop()

結果如下 : 




以上僅舉常見的元件為例, 其實所有的 tkinter 元件都可以用 Hovertip() 套上工具提示. 

參考 :




 

ChatGPT 無所不在

昨天去逛明儀, 發現一大堆書書名都標上 ChatGPT :




拿來翻一翻大都是新瓶裝舊酒, 大概只有最後面附上一章如何使用 ChatGPT 的內容而已. 

標到太陽能板 [補記]

上周陪二哥去建國路復興路口的原價屋買一台他要帶去中央讀碩班用的桌電 (主機 + 螢幕 41900 元), 周三通知可取貨, 但我晚上有課, 改週四晚上去取, 順便去長明街拿我標到太陽能板, 包含兩塊
近乎全新的 120W 能板 (共 3000 元) 與三塊 10W 二手能板 (共 730 元). 

周日早上將電線端絕緣皮除去, 在夏日陽光下量測開路電壓均正常, 120W 的打算放頂樓鐵皮屋頂, 10W 的則是屋子四周與菜園物聯網監控用. 銘牌如下 :





2023年7月2日 星期日

2023 年第 27 周記事

這一周都在忙著給維運軟體添加新功能, 雖然主要功能 (Telnet 傳送指令接收回應) 開工後三天就 OK, 但功能越加越多, 周五算是整體完工了. 經過這番鍛鍊才真正摸熟了 Tkinter 套件各元件的用法, 體會到只有實際去做 Project 才能真正了解一個技術. 這廂完事後, 緊接著還有一個要做, 估計還要 1~2 周的功夫, 但有 ChatGPT 輔助, 速度比以前快多了. 

周六早上提早回去鄉下郵局處理阿蘭的帳戶, 總算圓滿達成任務. 周六下午到後院一瞧, 樹上芒果所剩無幾, 今年豐收的芒果算是接近尾聲, 只剩四季芒果與另外一棵不知品種的較晚熟, 但應該 10 天內都會採完. 今年收了 1000 多顆, 都是挑好的送人吃, 皮相較差的留給自己. 特別是那株不知品種的果實很大, 尺寸與口感約與金煌差不多, 但表皮是紅色, 今天剖了一顆來吃, 發現別有風味. 納悶的是, 過去這麼多年我都沒注意到後院這棵特殊品種的芒果 (我都一直認為我家種的全是海頓), 從樹齡看是老欉啊, 應該是母親種植的, 為何這麼多年都沒有吃過的印象, 好奇怪.  

這兩天婷婷剛好放假, 傍晚天較不熱時都帶孩子過來騎腳踏車, 大家度過了一個愉快的周末. 有小孩的笑鬧聲真的很不錯. 

Python 學習筆記 : 用 pyinstaller 將 .py 程式轉成 .exe 執行檔

最近兩周用 tkinter 打造的維運自動化軟體完工後要佈署到 Windows 作業環境中來運行, 當然不能用開發測試階段那樣用命令列的 python 指令去執行原始碼, 因為那必須在作業電腦中也安裝 Python, 這種使用方式不僅麻煩 (還要開啟命令提示字元視窗), 而且有時作業電腦並不允許裝 Python. 可行的方法是將原始碼轉成 .exe 可執行檔, 它會自帶 Python 執行環境, 執行此程式的別台電腦不需要安裝 Python. 參考 : 



1. 安裝 pyinstall 套件 : 

用系統管理員身分開啟命令提示字元視窗安裝 pyinstaller 套件 (我第一次不是用系統管理員身分開啟命令提示字元視窗, 結果安裝失敗) : 

pip install pyinstaller   

C:\WINDOWS\system32>pip install pyinstaller     
Collecting pyinstaller
  Downloading pyinstaller-5.13.0-py3-none-win_amd64.whl (1.3 MB)
     ---------------------------------------- 1.3/1.3 MB 3.8 MB/s eta 0:00:00
Requirement already satisfied: setuptools>=42.0.0 in c:\python311\lib\site-packages (from pyinstaller) (65.5.0)
Collecting altgraph (from pyinstaller)
  Downloading altgraph-0.17.3-py2.py3-none-any.whl (21 kB)
Collecting pyinstaller-hooks-contrib>=2021.4 (from pyinstaller)
  Downloading pyinstaller_hooks_contrib-2023.4-py2.py3-none-any.whl (271 kB)
     ---------------------------------------- 271.8/271.8 kB 16.3 MB/s eta 0:00:00
Collecting pefile>=2022.5.30 (from pyinstaller)
  Downloading pefile-2023.2.7-py3-none-any.whl (71 kB)
     ---------------------------------------- 71.8/71.8 kB 4.1 MB/s eta 0:00:00
Collecting pywin32-ctypes>=0.2.1 (from pyinstaller)
  Downloading pywin32_ctypes-0.2.2-py3-none-any.whl (30 kB)
Installing collected packages: altgraph, pywin32-ctypes, pyinstaller-hooks-contrib, pefile, pyinstaller
Successfully installed altgraph-0.17.3 pefile-2023.2.7 pyinstaller-5.13.0 pyinstaller-hooks-contrib-2023.4 pywin32-ctypes-0.2.2


2. 執行 pyinstaller 將 .py 原始碼轉成 .exe : 

如果要將原始碼轉換成單一個 .exe 執行檔要在 pyinstaller 指令後面加一個 -F 參數, pyinstaller 會在目前目錄下建一個 dist 子目錄來存放此 exe 檔 : 

pyinstaller -F 原始碼檔案.py     

例如原始碼為 test.py : 

E:\test>pyinstaller -F test.py    
662 INFO: PyInstaller: 5.13.0
662 INFO: Python: 3.11.3
674 INFO: Platform: Windows-10-10.0.19045-SP0
726 INFO: wrote E:\test\test.spec
825 INFO: Extending PYTHONPATH with paths
['E:\\test']
1148 INFO: checking Analysis
1148 INFO: Building Analysis because Analysis-00.toc is non existent
1148 INFO: Initializing module dependency graph...
1157 INFO: Caching module graph hooks...
1187 INFO: Analyzing base_library.zip ...
3265 INFO: Loading module hook 'hook-heapq.py' from 'C:\\Python311\\Lib\\site-packages\\PyInstaller\\hooks'...
3389 INFO: Loading module hook 'hook-encodings.py' from 'C:\\Python311\\Lib\\site-packages\\PyInstaller\\hooks'...
5530 INFO: Loading module hook 'hook-pickle.py' from 'C:\\Python311\\Lib\\site-packages\\PyInstaller\\hooks'...
8728 INFO: Caching module dependency graph...
8908 INFO: running Analysis Analysis-00.toc
8935 INFO: Adding Microsoft.Windows.Common-Controls to dependent assemblies of final executable
  required by C:\Python311\python.exe
9018 INFO: Analyzing E:\test\test.py
9265 INFO: Loading module hook 'hook-idlelib.py' from 'C:\\Python311\\Lib\\site-packages\\PyInstaller\\hooks'...
9454 INFO: Loading module hook 'hook-difflib.py' from 'C:\\Python311\\Lib\\site-packages\\PyInstaller\\hooks'...
9802 INFO: Loading module hook 'hook-multiprocessing.util.py' from 'C:\\Python311\\Lib\\site-packages\\PyInstaller\\hooks'...
9975 INFO: Loading module hook 'hook-xml.py' from 'C:\\Python311\\Lib\\site-packages\\PyInstaller\\hooks'...
11103 INFO: Loading module hook 'hook-platform.py' from 'C:\\Python311\\Lib\\site-packages\\PyInstaller\\hooks'...
12725 INFO: Loading module hook 'hook-sysconfig.py' from 'C:\\Python311\\Lib\\site-packages\\PyInstaller\\hooks'...
12988 INFO: Processing module hooks...
13088 INFO: Loading module hook 'hook-_tkinter.py' from 'C:\\Python311\\Lib\\site-packages\\PyInstaller\\hooks'...
13090 INFO: checking Tree
13090 INFO: Building Tree because Tree-00.toc is non existent
13093 INFO: Building Tree Tree-00.toc
13297 INFO: checking Tree
13297 INFO: Building Tree because Tree-01.toc is non existent
13298 INFO: Building Tree Tree-01.toc
13337 INFO: checking Tree
13337 INFO: Building Tree because Tree-02.toc is non existent
13338 INFO: Building Tree Tree-02.toc
13379 INFO: Looking for ctypes DLLs
13427 INFO: Analyzing run-time hooks ...
13430 INFO: Including run-time hook 'C:\\Python311\\Lib\\site-packages\\PyInstaller\\hooks\\rthooks\\pyi_rth_inspect.py'
13433 INFO: Including run-time hook 'C:\\Python311\\Lib\\site-packages\\PyInstaller\\hooks\\rthooks\\pyi_rth_pkgutil.py'
13436 INFO: Including run-time hook 'C:\\Python311\\Lib\\site-packages\\PyInstaller\\hooks\\rthooks\\pyi_rth_multiprocessing.py'
13439 INFO: Including run-time hook 'C:\\Python311\\Lib\\site-packages\\PyInstaller\\hooks\\rthooks\\pyi_rth__tkinter.py'
13469 INFO: Looking for dynamic libraries
456 INFO: Extra DLL search directories (AddDllDirectory): []
456 INFO: Extra DLL search directories (PATH): ['C:\\Python311\\Scripts\\', 'C:\\Python311\\', 'C:\\Python37\\Scripts\\', 'C:\\Python37\\', 'C:\\Program Files (x86)\\Intel\\Intel(R) Management Engine Components\\iCLS\\', 'C:\\Program Files\\Intel\\Intel(R) Management Engine Components\\iCLS\\', 'C:\\WINDOWS\\system32', 'C:\\WINDOWS', 'C:\\WINDOWS\\System32\\Wbem', 'C:\\WINDOWS\\System32\\WindowsPowerShell\\v1.0\\', 'C:\\WINDOWS\\System32\\OpenSSH\\', 'C:\\Program Files (x86)\\Intel\\Intel(R) Management Engine Components\\DAL', 'C:\\Program Files\\Intel\\Intel(R) Management Engine Components\\DAL', 'C:\\Program Files\\Intel\\WiFi\\bin\\', 'C:\\Program Files\\Common Files\\Intel\\WirelessCommon\\', 'C:\\Program Files\\Git\\cmd', 'C:\\Program Files\\nodejs\\', 'C:\\ProgramData\\chocolatey\\bin', 'C:\\Users\\User\\AppData\\Local\\Microsoft\\WindowsApps', 'C:\\Program Files\\Bandizip\\', 'C:\\Users\\User\\AppData\\Local\\atom\\bin', 'C:\\Users\\User\\AppData\\Roaming\\npm']
14443 INFO: Looking for eggs
14450 INFO: Using Python library C:\Python311\python311.dll
14450 INFO: Found binding redirects:
[]
14469 INFO: Warnings written to E:\test\build\test\warn-test.txt
14510 INFO: Graph cross-reference written to E:\test\build\test\xref-test.html
15059 INFO: checking PYZ
15059 INFO: Building PYZ because PYZ-00.toc is non existent
15060 INFO: Building PYZ (ZlibArchive) E:\test\build\test\PYZ-00.pyz
17368 INFO: Building PYZ (ZlibArchive) E:\test\build\test\PYZ-00.pyz completed successfully.
17478 INFO: checking PKG
17478 INFO: Building PKG because PKG-00.toc is non existent
17480 INFO: Building PKG (CArchive) test.pkg
29536 INFO: Building PKG (CArchive) test.pkg completed successfully.
29609 INFO: Bootloader C:\Python311\Lib\site-packages\PyInstaller\bootloader\Windows-64bit-intel\run.exe
29609 INFO: checking EXE
29610 INFO: Building EXE because EXE-00.toc is non existent
29610 INFO: Building EXE from EXE-00.toc
29616 INFO: Copying bootloader EXE to E:\test\dist\test.exe.notanexecutable
29800 INFO: Copying icon to EXE
29805 INFO: Copying icons from ['C:\\Python311\\Lib\\site-packages\\PyInstaller\\bootloader\\images\\icon-console.ico']
29837 INFO: Writing RT_GROUP_ICON 0 resource with 104 bytes
29837 INFO: Writing RT_ICON 1 resource with 3752 bytes
29839 INFO: Writing RT_ICON 2 resource with 2216 bytes
29839 INFO: Writing RT_ICON 3 resource with 1384 bytes
29842 INFO: Writing RT_ICON 4 resource with 37019 bytes
29843 INFO: Writing RT_ICON 5 resource with 9640 bytes
29843 INFO: Writing RT_ICON 6 resource with 4264 bytes
29844 INFO: Writing RT_ICON 7 resource with 1128 bytes
29912 INFO: Copying 0 resources to EXE
29912 INFO: Embedding manifest in EXE
29914 INFO: Updating manifest in E:\test\dist\test.exe.notanexecutable
29952 INFO: Updating resource type 24 name 1 language 0
30012 INFO: Appending PKG archive to EXE
30339 INFO: Fixing EXE headers
32482 INFO: Building EXE from EXE-00.toc completed successfully.

我的 Python 環境是 3.11 版, 可見雖然上面參考的文章大都說 Python 3.9 以下都沒問題, 我實際測試 3.11 也是 OK 的. 轉換完畢會在目前資料夾下出現一個 dist 子目錄用來存放轉換得到的 .exe 檔 科切換到 dist 下看看是否能執行 :

E:\test>cd dist    

E:\test\dist>dir    
 磁碟區 E 中的磁碟沒有標籤。
 磁碟區序號:  03B3-666A

 E:\test\dist 的目錄

2023/07/02  上午 12:16    <DIR>          .
2023/07/02  上午 12:16    <DIR>          ..
2023/07/02  上午 12:16        12,179,107 test.exe   
               1 個檔案      12,179,107 位元組
               2 個目錄  399,272,574,976 位元組可用

要注意的是, 此 .exe 檔只包含執行原始碼的解譯環境, 並不包括程式會用到的資料夾或檔案, 因此若程式中有用到外部檔案 (例如 logo.ico 圖標檔案) 或目錄 (例如存放紀錄檔的 /log), 必須將它們從開發目錄複製到 dist 下面, 否則執行 test.exe 時會出現錯誤. 

注意, 如果是在檔案總管下點擊 test.exe, 發生錯誤時會閃退, 根本不知道發生甚麼事, 必須開啟命令提示字元視窗用命令列執行 test.exe 才會報出錯誤原因, 例如缺了圖標 logo.ico 會這樣 : 

E:\test\dist>test.exe   
Traceback (most recent call last):
  File "test.py", line 554, in <module>
  File "tkinter\__init__.py", line 2136, in wm_iconbitmap
_tkinter.TclError: bitmap "logo.ico" not defined
[21080] Failed to execute script 'test' due to unhandled exception!

補了 logo.ico 還缺了 /log 子目錄結果如下 :

E:\test\dist>test.exe   
Traceback (most recent call last):
  File "test.py", line 724, in <module>
  File "test.py", line 227, in load_log_list
FileNotFoundError: [WinError 3] 系統找不到指定的路徑。: './log'
[12768] Failed to execute script 'test' due to unhandled exception!

只要所需的檔案目錄都複製到 dist 下面後就可以順利執行了. 如果要以綠色軟體方式執行, 只要將 dist 目錄複製到別台電腦, 然後將 exe 傳送到桌面當捷徑, 點擊捷徑即可執行無須安裝; 如果需要打包成安裝程式, 可以用 Inno setup 之類的打包軟體來將整個 dist 下的程式, 檔案, 與目錄打包成單一個 setup.exe, 參考 :



2023-07-04 補充 : 

最近幾天數據處理工作量較大, 所幸這套軟體及時派上用場, 自動化讓作業速度提升好幾倍, 不需要像以前那樣用 Excel 或 EditPlus 修修改改, 花這兩周的時間太值得啦! 不過程式執行時會先出現一個無內容的命令提示字元視窗 (我猜是用來啟動 Python 環境), 然後才出現 Tkinter 視窗 : 




我從下面這篇文章得知, 原來在轉換時可以用 -w 參數讓命令提示字元視窗不要出現 : 


例如 : 

pyinstaller -F 原始碼檔案.py  -w      

兩周內改版至第 15 版, 原始碼 1200 行左右, 約 57KB, 轉成 .exe 檔後約 12MB. 


2023-07-24 補充 : 

經過 4 周我又寫好一個 tkinter 的 GUI 軟體 (CSR 相關), 為了避開資安查核要求, 程式不另外用 SETUP 軟體打包, 均以 portable 方式執行, 在佈署時直接將主程式傳送到桌面當捷徑, 這時就需要一個 logo.ico 來在桌面標示此程式, 這可以用小畫家等工具製作一個 32x32 的 .bmp 圖標檔, 利用下面的線上轉檔工具轉成 .ico 檔 :


如果需要將圖標圓角化, 可以先將圖檔用下列網站處理 :


參考 :


不過此 .ico 是在程式執行時顯示在左上角作為圖標, 並不會在桌面顯示, 如果要將此 logo.ico 也當作桌面捷徑圖標, 在用 pyinstaller 時需加入 --icon=logo.ico 參數, 例如 : 

pyinstaller --onefile --icon=logo.ico 原始碼檔案.py -w