2023年6月29日 星期四

料理實驗 : 水煮茄子不變黑秘訣

最近在臉書食譜社群看到有人分享水煮茄子不變黑的秘訣, 作法如下 : 
  1. 茄子切段後對半切開, 水滾後再將茄子下鍋, 紫色朝下切口朝上, 這樣便能阻絕表皮與空氣接觸而氧化, 保持鮮豔的藍紫色. 鍋蓋可以蓋上也可不蓋 (蓋上較快煮熟), 也不需要放鹽或油, 用水煮至軟熟後即可盛盤.
  2. 醬料調製 : 蒜粒壓扁切碎, 九層塔葉洗淨切碎, 加入一小匙鹽, 糖, 薄鹽醬油, 少許香油與開水攪拌, 淋在煮熟的茄子上或用沾的亦可.  




炎炎夏日, 這鮮豔的藍紫色水煮茄子蠻引動人食慾的. 

市圖還書 10 本 (C & C++ 與理財書)

今天還了一批 C & C++ 的書 : 
6 月初教完五堂 C 語言課覺得好累, 備課不累, 而是學生的學習態度讓我很累, 把這些書都還了吧, 短時間內不會用到 C. 

2023年6月28日 星期三

2023 年第 26 周記事

哇哇, 時序來到第 26 周, 一年已經過了一半, 查看退休計時器, 即將進入 8 年倒數 :




突然感覺到初老的恐慌啊! 

芒果本周已經採得差不多了 (感謝小舅與舅媽幫忙), 樹上果子所剩不多, 周日下午小舅採了兩桶回去後, 我又拿了採果竿到後院籬笆外以及路邊去採, 同樣收穫了兩桶 :





周四端午節早上要祭祖, 我與菁菁, 水某周三晚上回到鄉下 (姊姊忙工作沒回高雄, 二哥確診休息), 因為菁菁與水某周五要上班, 故周四晚上載她們回高雄, 周六早上我又回鄉下, 總共跑了三趟. 因為這樣休四天都沒時間去鎮上買菜, 只好清一清冰箱的存貨, 結果周六傍晚琪大師與公子突然造訪才發現食材窘迫 (沒青菜了), 勉強才端出四樣菜來, 太寒酸了. 

周日早上到市場順便去圖書館還書, 出來時經過全家門口跟擺豆腐攤的老闆買一杯豆花, 突然想到這家全家有霜淇淋機, 每次都看菁菁吃 (我開車啊), 何不吃看看到底哪裡好吃, 於是就進去買了一杯 (49 元), 坐在鄰近廣場的位置好好品嚐 :




想做的事情不要等有空, 人生要隨時都有空. 

2023年6月24日 星期六

Python 學習筆記 : 如何在 Tkinter 中使用 HTML

今天在 Youtube 看到下面這個教學影片, 介紹如何利用第三方套件 tkhtmlview 在 Tkiner 視窗中使用 HTML + CSS 語法來美化視窗內容與連結網站 :





我覺得這功能很實用, 那就依樣畫葫蘆來測試一番唄!

首先用 pip install 來安裝 tkhtmlview, 注意, 要用系統管理員身分開啟命令提示字元視窗 :




Microsoft Windows [版本 10.0.19045.3086]
(c) Microsoft Corporation. 著作權所有,並保留一切權利。

C:\WINDOWS\system32>pip install tkhtmlview    
Collecting tkhtmlview
  Using cached tkhtmlview-0.2.0-py3-none-any.whl (10 kB)
Requirement already satisfied: Pillow<10.0.0,>=9.4.0 in c:\python311\lib\site-packages (from tkhtmlview) (9.5.0)
Collecting requests<3.0.0,>=2.28.2 (from tkhtmlview)
  Using cached requests-2.31.0-py3-none-any.whl (62 kB)
Requirement already satisfied: charset-normalizer<4,>=2 in c:\python311\lib\site-packages (from requests<3.0.0,>=2.28.2->tkhtmlview) (3.1.0)
Requirement already satisfied: idna<4,>=2.5 in c:\python311\lib\site-packages (from requests<3.0.0,>=2.28.2->tkhtmlview) (3.4)
Requirement already satisfied: urllib3<3,>=1.21.1 in c:\python311\lib\site-packages (from requests<3.0.0,>=2.28.2->tkhtmlview) (2.0.3)
Collecting certifi>=2017.4.17 (from requests<3.0.0,>=2.28.2->tkhtmlview)
  Using cached certifi-2023.5.7-py3-none-any.whl (156 kB)
Installing collected packages: certifi, requests, tkhtmlview
Successfully installed certifi-2023.5.7 requests-2.31.0 tkhtmlview-0.2.0

C:\WINDOWS\system32>

如果沒有用系統管理員身分開啟命令提示字元視窗, 則安裝時可能會出現無法寫入錯誤而失敗 : 

Installing collected packages: urllib3, Pillow, idna, charset-normalizer, certifi, requests, tkhtmlview
  WARNING: Failed to write executable - trying to use .deleteme logic
ERROR: Could not install packages due to an OSError: [WinError 2] 系統找不到指定的檔案。: 'C:\\Python311\\Scripts\\normalizer.exe' -> 'C:\\Python311\\Scripts\\normalizer.exe.deleteme'

目前 tkhtmlview 支援的 HTML 標籤尚未涵蓋全部 HTML5, 但常用的標籤例如 H1~H6, A, P 等都可使用, 參考教學文件 :



1. 文字效果 :  

下面範例使用 Label 元件來測試 H1~H6 的文字效果 : 


測試 1 : 在 HTMLLabel 元件上顯示 HTML 的 H1~H6 文字效果 

import tkinter as tk
from tkhtmlview import HTMLLabel

root=tk.Tk()
root.geometry('600x400')
html_1='<h1 style="color: red; text-align: center"> Hello World </h1>'
label_1=HTMLLabel(root, html=html_1)
label_1.pack()
label_1.fit_height()
html_2='<h2 style="color: green; text-align: center"> Hello World </h2>'
label_2=HTMLLabel(root, html=html_2)
label_2.pack()
label_2.fit_height()
html_3='<h3 style="color: blue; text-align: center"> Hello World </h3>'
label_3=HTMLLabel(root, html=html_3)
label_3.pack()
label_3.fit_height()
html_4='<h4 style="color: pink; text-align: center"> Hello World </h4>'
label_4=HTMLLabel(root, html=html_4)
label_4.pack()
label_4.fit_height()
html_5='<h5 style="color: cyan; text-align: center"> Hello World </h5>'
label_5=HTMLLabel(root, html=html_5)
label_5.pack()
label_5.fit_height()
html_6='<h6 style="color: black; text-align: center"> Hello World </h6>'
label_6=HTMLLabel(root, html=html_6)
label_6.pack()
label_6.fit_height()
root.mainloop()

此例使用 H1~H6 在 HTMLLabel 元件上顯示 6 種不同大小的標題, 注意, 連續用 pack() 將 HTMLLabel 放置到 root 容器時因為預設高度較大,  後面的元件會被推到是窗外而看不到, 因此必須呼叫 HTMLLabel 物件的 fit_height() 方法來調適元件高度, 結果如下 : 




HTMLLabel 已支援多國語言 (即多位元組字元例如中文) , 若將上面程式中的 "Hello World" 改成 "你是在說哈囉嗎?" 結果如下 :




2. 顯示超連結 :  

下面範例測試超連結 A 標籤 :


測試 2 : 在 HTMLLabel 元件上顯示超連結

import tkinter as tk
from tkhtmlview import HTMLLabel

root=tk.Tk()
root.geometry('400x200')
html_1='<a href="https://tw.yahoo.com/" target="_blank">雅虎奇摩</a>'
label_1=HTMLLabel(root, html=html_1)
label_1.pack(padx=5, pady=5)
label_1.fit_height()
html_2='<a href="https://www.google.com.tw/" target="_blank">Google</a>'
label_2=HTMLLabel(root, html=html_2)
label_2.pack(padx=5, pady=5)
label_2.fit_height()
html_3='<a href="https://chateverywhere.app/zh" target="_blank">Chat Everwhere</a>'
label_3=HTMLLabel(root, html=html_3)
label_3.pack(padx=5, pady=5)
label_3.fit_height()
root.mainloop()

此例在 HTMLLabel 元件上顯示 Yahoo, Google, 與 Chat Everywhere 的網址超連結, 由於使用了 target='_blank' 屬性, 因此點擊超連結會在瀏覽器中開啟新分頁來顯示網頁, 結果如下 :



 
這個功能在 Tkinter GUI 視窗需要連結網站時非常好用. 


3. 顯示清單 :  

tkhtmlview 支援 OL, UL, LI 等清單標籤, 例如 :

測試 3 : 在 HTMLLabel 元件上顯示清單超連結

import tkinter as tk
from tkhtmlview import HTMLLabel

root=tk.Tk()
root.geometry('400x400')
html='''
<ol>
  <li><a href="https://tw.yahoo.com/" target="_blank">雅虎奇摩</a></li>
  <li><a href="https://www.google.com.tw/" target="_blank">Google</a></li>
  <li><a href="https://chateverywhere.app/zh" target="_blank">Chat Everwhere</a></li>
</ol>
<ul>
  <li><a href="https://tw.yahoo.com/" target="_blank">雅虎奇摩</a></li>
  <li><a href="https://www.google.com.tw/" target="_blank">Google</a></li>
  <li><a href="https://chateverywhere.app/zh" target="_blank">Chat Everwhere</a></li>
</ul>
'''
label=HTMLLabel(root, html=html)
label.pack(padx=5, pady=5)
label.fit_height()
root.mainloop()

此例分別用 ol 與 ul 元素來呈現編號與無編號清單, 結果如下 :




4. 顯示圖片 :  

接下來測試 IMG 標籤的圖片顯示功能, 主要是透過其 src 屬性來指定圖片來源, 可以是網址或圖片在本機中的相對路徑, 下面是顯示本機程式工作目錄例如 : 


測試 4 : 在 HTMLLabel 元件上顯示圖片

import tkinter as tk
from tkhtmlview import HTMLLabel

root=tk.Tk()
root.geometry('800x700')
html_1='<img src="orchid.jpg">'
label_1=HTMLLabel(root, html=html_1)
label_1.pack(padx=5, pady=5, anchor=tk.CENTER)
label_1.fit_height()
label_2=HTMLLabel(root, html=html_2)
label_2.pack(padx=125, pady=5)
label_2.fit_height()
root.mainloop()

此例在視窗中放置兩張圖片, 第一張圖片來源為本機程式目錄下的 orchid.jpg; 第二張是放在 GitHub 的同一張圖片, 分別使用 anchor 與 padx 參數來控制圖片置中, 結果如下 :




可見 anchor 參數對於 HTMLLabel 上的圖片沒有作用, 只能用 padx 調整其水平位置. 另外, 用來給圖片加上文字標題的 figure 標籤目前也尚未支援. 


5. 顯示表格 (支援度不全) :  

tkhtmlview 對表格的支援還不足 (雖然 colspan 合併儲存格似乎可用), 連 border 都無法顯示, 例如下列範例 :


測試 5 : 在 HTMLLabel 元件上顯示表格

import tkinter as tk
from tkhtmlview import HTMLLabel

root=tk.Tk()
root.geometry('800x400')
html='''
    <table border=1>
      <caption><strong>表格標題</strong></caption>
      <tr>
        <th>第1欄</th>
        <th>第2欄</th>
        <th>第3欄</th>
      </tr>
      <tr>
        <td>第1列第1欄</td>
        <td>第1列第2欄</td>
        <td>第1列第3欄</td>
      </tr>
      <tr>
        <td>第2列第1欄</td>
        <td>第2列第2欄</td>
        <td>第2列第3欄</td>
      </tr>
      <tr>
        <td colspan="3">合併儲存格</td>
      </tr>
    </table>'''
label=HTMLLabel(root, html=html)
label.pack(padx=5, pady=5)
label.fit_height()
root.mainloop()

此處用 border=1 設定表格邊框但無作用, 結果如下 : 




希望未來 tkhtmlview 可以完善對 TABLE 的支援. 


6. 用 HTMLText 與 HTMLRender 顯示 HTML 檔內容 :  

除了用 HTMLLabel 元件直接顯示 HTML 碼外, tkhtmlview 也可以透過 HTMLRender 物件讀取外部 HTML 檔顯示於視窗中, 例如下面這個網頁 index.htm :


測試 6 : 用 HTMLRender 與 HTMLText 讀取 HTML 檔

<!DOCTYPE html>
<html>
  <head>
    <title></title>
    <meta charset="utf-8">
  </head>
  <body>
    <h2>Hello World!</h2>
    <img src="orchid.jpg">
  </body>
</html>

將其存放於程式工作目錄下, 然後執行下列程式碼 :

import tkinter as tk
from tkhtmlview import HTMLText, RenderHTML

root=tk.Tk()
root.geometry('800x500')
label=HTMLText(root, html=RenderHTML('index.htm'))
label.pack(padx=5, pady=5)
label.fit_height()
root.mainloop()

結果如下 :




但目前 HTMLRender 物件僅限於顯示英文資料, 無法像 HTMLLabel 那樣顯示中文內容, 例如若若將上面 index.htm 檔的 H2 標籤內容從 "Hello World!" 改為 "你是在說哈囉嗎?" 時就會顯示如下錯誤 : 

UnicodeDecodeError: 'cp950' codec can't decode byte 0xa0 in position 142: illegal multibyte sequence

可能 HTMLRender 物件尚未支援 Unicode. 

2023年6月23日 星期五

露天購買威剛 DDR3 1600 8GB 記憶體

上週查出鄉下老家第二台桌機 (技嘉) 主機板上的記憶體規格為 DDR3 1600 後, 上露天找到下面這賣家有賣各品牌 DDR3 模組, 本想買與目前主板上同樣的 Kinston $300 元, 但過了一周賣掉了, 所以改買威剛 8GB : 





小七免運 350 元. 

本想買廣穎 $300, 但看到下面這篇, 還是買威剛好了 :


Python 學習筆記 : 如何設定 Tkinter 視窗的圖標 (icon)

Tkinter 視窗預設的圖標 (icon) 是一個羽毛, 以下面簡單的空視窗程式為例 :

import tkinter as tk
from tkinter import ttk

# 建立視窗
root=tk.Tk()  # 建立視窗物件
root.title('測試') # 設定視窗標題
root.geometry('300x200')  # 設定視窗尺寸

root.mainloop()

執行後可看到視窗左上角的預設羽毛圖標 :




如果要更改為自訂的圖標該怎麼做? 首先用小畫家或其他圖像編輯軟體製作一個 16*16 的圖檔, 例如我用 Tony 的 T 作為圖標 (logo.bmp) : , 然後用線上轉檔軟體轉成 .ico 檔, 例如 :


雖然此網站標題寫 'png 轉 ico', 其實 jpg 或 bmp 等都是可以的 : 


轉完後下載 logo,ico, 將其放在程式目錄下, 然後改寫程式, 呼叫根視窗物件的 iconbitmap() 方法並傳入圖標的檔名即可 :

import tkinter as tk
from tkinter import ttk

# 建立視窗
root=tk.Tk()  # 建立視窗物件
root.title('測試') # 設定視窗標題
root.geometry('300x200')  # 設定視窗尺寸
root.iconbitmap('logo.ico')  
root.mainloop()

結果如下 :




2023-07-16 補充 :

如果程式搬移時 .ico 檔沒有隨同搬移, 則執行時會因為讀不到檔案而發生例外, 因此需用 try except 捕捉, 例如 :

import tkinter as tk
from tkinter import ttk

# 建立視窗
root=tk.Tk()  # 建立視窗物件
root.title('測試') # 設定視窗標題
root.geometry('300x200')  # 設定視窗尺寸
try:  # 避免沒有 logo 時錯誤
    root.iconbitmap('logo.ico')  
except Exception as e:
    pass
root.mainloop()

這樣當 logo.ico不存在時就會使用預設的 .ico. 

匯出 Google 傳統版協作平台內容

我以前使用 Google 協作平台當作網路私人記事本, 用來記錄每天看到的具有參考價值之資訊, 但最近瀏覽協作平台卻發現無法使用, 原來 Google 已經在 6/10 將協作平台網頁刪除, 但一個月內可匯出並下載原先儲存在平台上的內容 :




按 Download 可預約下載作業 : 




下載網址會寄到 Gmail 信箱. 

雖然去年停止協作平台時 Google 已將平台網頁內容轉移到 Google 雲端硬碟, 但我不確定有無包含上傳到協作平台上的檔案 (待確認), 所以還是將所有內容都下載為宜. 

2023年6月22日 星期四

Python 學習筆記 : Tkinter 的版面布局容器 Frame 與 PanedWindow

雖然 Frame 物件我還沒時間做一個較完整的測試, 但這周在用 Tkinter 開發公司的維運小軟體時直接拿來上陣使用, 發覺用 Frame 元件搭配最簡單常用的 pack 版面布局也可以將會疊串在一起的 Entry, RadionButton, CheckButton, 與 Button 等元件水平擺放得很整齊.

下面以簡化的系統登入與操作按鈕等元件來說明 Frame 在排版上的妙用. 視窗中包含帳號密碼輸入框, 登入按鈕, 指令輸入框, 執行按鈕, 以及一個執行結果文字區域等元件 : 

 
測試 1 : pack() 的堆疊式的版面布局 

import tkinter as tk
from tkinter import ttk

def login():
    pass

def run():
    pass

# 建立視窗
root=tk.Tk()  # 建立視窗物件
root.title('Frame + Pack 布局測試') # 設定視窗標題
root.geometry('800x600')  # 設定視窗尺寸

# 建立登入介面與指令輸入介面
account=tk.Entry(root, width=10)  # 帳號輸入框
account.pack()  # 元件放入視窗
password=tk.Entry(root, width=10, show='*')  # 密碼輸入框
password.pack()  # 元件放入視窗
login_btn=tk.Button(root, text='登入', command=login)  # 登入按鈕
login_btn.pack()  # 元件放入視窗
command=tk.Entry(root, width=20)  # 指令輸入框
command.pack()  # 元件放入視窗
run_btn=tk.Button(root, text='執行', command=run)  # 登入按鈕
run_btn.pack()  # 元件放入視窗
# 建立執行結果文字區域
result=tk.Text(root)
result.pack(fill=tk.BOTH, expand=True)
result.pack()

root.mainloop()

結果如下 : 




可見 pack 版面管理員的布局方式基本上就是由上而下堆疊, 雖然可以在呼叫 pack() 時傳入 side=tk.RIGHT/LEFT/TOP/BOTTOM 指定上下左右之垂直水平位置, 或傳入 anchor=tk.E/W/S/N/CENTER/NW/NE/SW/SE 來定位元件在父容器中的位置, 但光用 side 與 anchor 參數無法達成讓上方輸入框與按鈕元件以水平方式排在最上方, 且登入在左, 執行在右的需求. 

解決辦法之一是使用 Frame 容器來放置元件, 並且每個元件在呼叫 pack() 時傳入 side=tk.LEFT 指定由左向右水平排列, 版面結構如下圖所示 :




測試 2 : 改用 Frame 來當元件容器 

import tkinter as tk
from tkinter import ttk

def login():
    pass

def run():
    pass

# 建立視窗
root=tk.Tk()  # 建立視窗物件
root.title('Frame + Pack 布局測試') # 設定視窗標題
root.geometry('800x600')  # 設定視窗尺寸

# 建立登入介面與指令輸入介面
frame=tk.Frame(root)  # 外部 Frame 元件
frame.pack()
# 建立以 Frame 為父容器的元件
account=tk.Entry(frame, width=10)  # 帳號輸入框
account.pack(side=tk.LEFT)  # 元件放入視窗
password=tk.Entry(frame, width=10, show='*')  # 密碼輸入框
password.pack(side=tk.LEFT)  # 元件放入視窗
login_btn=tk.Button(frame, text='登入', command=login)  # 登入按鈕
login_btn.pack(side=tk.LEFT)  # 元件放入視窗
command=tk.Entry(frame, width=20)  # 指令輸入框
command.pack(side=tk.LEFT)  # 元件放入視窗
run_btn=tk.Button(frame, text='執行', command=run)  # 登入按鈕
run_btn.pack(side=tk.LEFT)  # 元件放入視窗
# 建立執行結果文字區域
result=tk.Text(root)
result.pack(fill=tk.BOTH, expand=True)
result.pack()

root.mainloop()

此例先建立一個 Frame 元件, 然後先後建立以此 Frame 元件為父容器 (不是 root 了) 的輸入框 Entry 與 Button 按鈕等元件, 但 Text 元件依然是以 root 為父元件. 結果如下 :



 
可見輸入框與按鈕都在 Frame 內呈水平排列. 注意, Frame 內的所有元件呼叫 pack() 時均傳入 tk.LEFT, 這表示元件是在 Frame 內由左向右依序排列之意, 並非定位在左邊 (定位要用 ANCHOR 參數); 同理, tk.RIGHT 表示由右向左排列. 如果將上面範例的 command 與 run_btn 兩個元件的 pack() 改為傳入 tk.RIGHT 會讓此兩元件順序顛倒 : 


測試 3 : 呼叫 pack() 傳入 side=tk.RIGHT 為由右向左排列

import tkinter as tk
from tkinter import ttk

def login():
    pass

def run():
    pass

# 建立視窗
root=tk.Tk()  # 建立視窗物件
root.title('Frame + Pack 布局測試') # 設定視窗標題
root.geometry('800x600')  # 設定視窗尺寸

# 建立登入介面與指令輸入介面
frame=tk.Frame(root)  # 外部 Frame 元件
frame.pack()

account=tk.Entry(frame, width=10)  # 帳號輸入框
account.pack(side=tk.LEFT)  # 元件放入視窗
password=tk.Entry(frame, width=10, show='*')  # 密碼輸入框
password.pack(side=tk.LEFT)  # 元件放入視窗
login_btn=tk.Button(frame, text='登入', command=login)  # 登入按鈕
login_btn.pack(side=tk.LEFT)  # 元件放入視窗
command=tk.Entry(frame, width=20)  # 指令輸入框
command.pack(side=tk.RIGHT)  # 元件放入視窗
run_btn=tk.Button(frame, text='執行', command=run)  # 登入按鈕
run_btn.pack(side=tk.RIGHT)  # 元件放入視窗
# 建立執行結果文字區域
result=tk.Text(root)
result.pack(fill=tk.BOTH, expand=True)
result.pack()

root.mainloop()

此例程式碼與上面不同之處僅在於 command 與 run_btn 這兩個元件呼叫 pack() 時傳入 side=tk.RIGHT 而已, 結果兩個元件就由右向左排列 :




所以放入 Frame 容器的元件呼叫 pack() 時一律傳入 side_tk.LEFT 即可. 

如果要讓這些 Frame 內水平排列的元件有所區隔, 可以用一個無顯示字串的 Label 來當隱形的佔位元件, 可透過 width (字元數, 不是 px) 來控制間隔距離, 例如 :


測試 4 : 利用無顯示字串的 Label 元件佔位

import tkinter as tk
from tkinter import ttk

def login():
    pass

def run():
    pass

# 建立視窗
root=tk.Tk()  # 建立視窗物件
root.title('Frame + Pack 布局測試') # 設定視窗標題
root.geometry('800x600')  # 設定視窗尺寸

# 建立登入介面與指令輸入介面
frame=tk.Frame(root)  # 外部 Frame 元件
frame.pack()

account=tk.Entry(frame, width=10)  # 帳號輸入框
account.pack(side=tk.LEFT)  # 元件放入視窗
password=tk.Entry(frame, width=10, show='*')  # 密碼輸入框
password.pack(side=tk.LEFT)  # 元件放入視窗
login_btn=tk.Button(frame, text='登入', command=login)  # 登入按鈕
login_btn.pack(side=tk.LEFT)  # 元件放入視窗
placeholder=tk.Label(frame, width=10)  # 佔位用的 Label
placeholder.pack(side=tk.LEFT)
command=tk.Entry(frame, width=20)  # 指令輸入框
command.pack(side=tk.LEFT)  # 元件放入視窗
run_btn=tk.Button(frame, text='執行', command=run)  # 登入按鈕
run_btn.pack(side=tk.LEFT)  # 元件放入視窗
# 建立執行結果文字區域
result=tk.Text(root)
result.pack(fill=tk.BOTH, expand=True)
result.pack()

root.mainloop()

此例在 login 鈕與 command 輸入框之間插入一個無顯示字串 (即 text='') 且設定 width=10 的 Lable 元件當佔位元件 (空元件), 這樣就在兩元件之間製造了 10 個字元寬度的間隔, 結果如下 :




注意, 間隔寬度若設太大, 撐破 Frame 的寬度時, 後面的元件可能會跳到下一行而影響版面外觀. 

除了 Frame 容器外, 也可以用 PanedWindow 來做版面布局, 茲將上面範例中的 Frame 改成 PanedWindow, 結果相同 :


測試 5 : 使用 PanedWindow 排版 

import tkinter as tk
from tkinter import ttk

def login():
    pass

def run():
    pass

# 建立視窗
root=tk.Tk()  # 建立視窗物件
root.title('pw + Pack 布局測試') # 設定視窗標題
root.geometry('800x600')  # 設定視窗尺寸

# 建立登入介面與指令輸入介面
pw=tk.PanedWindow(root)  # 外部 pw 元件
pw.pack()

account=tk.Entry(pw, width=10)  # 帳號輸入框
account.pack(side=tk.LEFT)  # 元件放入視窗
password=tk.Entry(pw, width=10, show='*')  # 密碼輸入框
password.pack(side=tk.LEFT)  # 元件放入視窗
login_btn=tk.Button(pw, text='登入', command=login)  # 登入按鈕
login_btn.pack(side=tk.LEFT)  # 元件放入視窗
placeholder=tk.Label(pw, width=10)  # 佔位用的 Label
placeholder.pack(side=tk.LEFT)
command=tk.Entry(pw, width=20)  # 指令輸入框
command.pack(side=tk.LEFT)  # 元件放入視窗
run_btn=tk.Button(pw, text='執行', command=run)  # 登入按鈕
run_btn.pack(side=tk.LEFT)  # 元件放入視窗
# 建立執行結果文字區域
result=tk.Text(root)
result.pack(fill=tk.BOTH, expand=True)
result.pack()

root.mainloop()

此例建立一個 PanedWindow 物件 pw, 然後底下程式碼中的 frame 全部改成 pw, 結果如下 :




可見結果與上面用 Frame 排版的效果一模一樣. 

除了直接呼叫元件的 pack() 方法將元件加入父容器 PanedWindow 物件外, 也可以呼叫該物件的 add() 方法, 它預設也是用 tk.LEFT 由左向右將元件放入容器中, 例如 :


測試 6 : 使用 PanedWindow 的 add() 方法將元件加入容器 

import tkinter as tk
from tkinter import ttk

def login():
    pass

def run():
    pass

# 建立視窗
root=tk.Tk()  # 建立視窗物件
root.title('pw + Pack 布局測試') # 設定視窗標題
root.geometry('800x600')  # 設定視窗尺寸

# 建立登入介面與指令輸入介面
pw=tk.PanedWindow(root)  # 外部 pw 元件
pw.pack()

account=tk.Entry(pw, width=10)  # 帳號輸入框
pw.add(account)  # 元件放入視窗
password=tk.Entry(pw, width=10, show='*')  # 密碼輸入框
pw.add(password)  # 元件放入視窗
login_btn=tk.Button(pw, text='登入', command=login)  # 登入按鈕
pw.add(login_btn)  # 元件放入視窗
placeholder=tk.Label(pw, width=10)  # 佔位用的 Label
pw.add(placeholder)
command=tk.Entry(pw, width=20)  # 指令輸入框
pw.add(command)  # 元件放入視窗
run_btn=tk.Button(pw, text='執行', command=run)  # 登入按鈕
pw.add(run_btn)  # 元件放入視窗
# 建立執行結果文字區域
result=tk.Text(root)
result.pack(fill=tk.BOTH, expand=True)
result.pack()

root.mainloop()

結果與上面範例相同. 

Python 學習筆記 : Tkinter 帶捲軸 Treeview 的位置問題

最近幾天在用 Tinker 開發公司維運軟體時發現了幾個過去不曾注意的小問題, 但這些細節卻足以讓我耗掉無謂的時間在 debug 上面, 例如要幫 Treview 綁定一個右捲軸時, 捲軸居然出現在 Treeview 下方而不是右方, 重新檢視 API 說明與比對之前成功範例都沒錯啊, 怪了. 

後來我將原始碼貼給 ChatGPT 詢問到底問題出在哪裡, 原來正解是 : 捲軸必須在 TreeView 之前放置 (pack), 亦即要先呼叫捲軸的 pack(), 然後再呼叫 TreeView 的 pack(), 範例如下 :

測試 1 : 正確的 Treeview + Scrollbar 用法 : 

import tkinter as tk
from tkinter import ttk

# 建立視窗
root=tk.Tk()  # 建立視窗物件
root.title('TreeView 與 Scrollbar 測試') # 設定視窗標題
root.geometry('600x200')  # 設定視窗尺寸
# 建立捲軸與 Treeview
scrollbar=tk.Scrollbar(root)  # 建立捲軸物件
scrollbar.pack(side=tk.RIGHT, fill=tk.Y) # 卷軸必須比 Treeview 先放置
treeview=ttk.Treeview(root, columns=('country', 'continent')) # 建立 Treeview
treeview.heading('#0', text='國碼')  # 設定 icon 欄標題
treeview.heading('#1', text='國家')  # 設定第二欄標題 
treeview.heading('#2', text='洲別')  # 設定第三欄標題
# 設定 Treeview 內容 : text 是 icon 欄位, vales 是其他欄位
treeview.insert('', index=tk.END, text='81', values=['日本', '亞洲'])
treeview.insert('', index=tk.END, text='31', values=['荷蘭', '歐洲'])
treeview.insert('', index=tk.END, text='249', values=['蘇丹', '非洲'])
treeview.insert('', index=tk.END, text='886', values=['台灣', '亞洲'])
treeview.insert('', index=tk.END, text='65', values=['新加坡', '亞洲'])
treeview.insert('', index=tk.END, text='44', values=['英國', '歐洲'])
treeview.insert('', index=tk.END, text='501', values=['貝里斯', '美洲'])
treeview.insert('', index=tk.END, text='66', values=['泰國', '亞洲'])
treeview.insert('', index=tk.END, text='504', values=['宏都拉斯', '美洲'])
treeview.insert('', index=tk.END, text='850', values=['北韓', '亞洲'])
treeview.pack(fill=tk.BOTH, expand=True)  # Treeview 須在捲軸後放置
# 捲軸與 Treeview 互相綁定
treeview.config(yscrollcommand=scrollbar.set)  # Treeview 綁定右捲軸
scrollbar.config(command=treeview.yview)  # 捲軸綁定 Treeview

root.mainloop()
 
注意此處的關鍵在於 Scrollbar 須比 Treeview 先放到 root 容器中, 結果如下 :




可見捲軸正確地出現在 Treeview 右邊. 如果 pack() 的順序顛倒, 即先放 Treeview 後放 Scrollbar, 那麼 Scrollbar 會出現在 Treeview 下方, 例如 :


測試 1 : 不正確的 Treeview + Scrollbar 用法 : 

import tkinter as tk
from tkinter import ttk

# 建立視窗
root=tk.Tk()  # 建立視窗物件
root.title('TreeView 與 Scrollbar 測試') # 設定視窗標題
root.geometry('600x300')  # 設定視窗尺寸
# 建立捲軸與 Treeview
treeview=ttk.Treeview(root, columns=('country', 'continent')) # 建立 Treeview
treeview.heading('#0', text='國碼')  # 設定 icon 欄標題
treeview.heading('#1', text='國家')  # 設定第二欄標題 
treeview.heading('#2', text='洲別')  # 設定第三欄標題
# 設定 Treeview 內容 : text 是 icon 欄位, vales 是其他欄位
treeview.insert('', index=tk.END, text='81', values=['日本', '亞洲'])
treeview.insert('', index=tk.END, text='31', values=['荷蘭', '歐洲'])
treeview.insert('', index=tk.END, text='249', values=['蘇丹', '非洲'])
treeview.insert('', index=tk.END, text='886', values=['台灣', '亞洲'])
treeview.insert('', index=tk.END, text='65', values=['新加坡', '亞洲'])
treeview.insert('', index=tk.END, text='44', values=['英國', '歐洲'])
treeview.insert('', index=tk.END, text='501', values=['貝里斯', '美洲'])
treeview.insert('', index=tk.END, text='66', values=['泰國', '亞洲'])
treeview.insert('', index=tk.END, text='504', values=['宏都拉斯', '美洲'])
treeview.insert('', index=tk.END, text='850', values=['北韓', '亞洲'])
treeview.pack(fill=tk.BOTH, expand=True)  # Treeview 須在捲軸後放置
scrollbar=tk.Scrollbar(root)  # 建立捲軸物件
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)  # 卷軸比 Treeview 後放置會出現在下方
treeview.config(yscrollcommand=scrollbar.set)  # Treeview 綁定右捲軸
scrollbar.config(command=treeview.yview)  # 捲軸綁定 Treeview

root.mainloop()

此例程式碼內容與上面完全相同, 差別僅在於 Scrollbar 呼叫 pack() 順序在 Treeview 呼叫 pack() 之後, 結果捲軸會被放在 Treeview 下方, 如下圖所示 :




順序決定了位置, 人生也是如此. 

2023年6月20日 星期二

高科大還書 1 本 (使用C語言的10堂入門程式課)

因為向母校預約的 '大數據淘金術 : Python機器學習高手實彈演練' 這本書已到館, 而暑假即將在下周開始, 借還書都必須利用中午午休, 所以趁本周還有開到晚上 10 點的機會去取書, 但滿借須還 1 本, 剛好 C 語言授課結束, 就拿下面這本去換 : 



Source : 博客來


此書編得非常簡潔容易學習, 指標, 結構等課題都有介紹,  感覺比我授課的那本還要好. 此書作者吳燦銘應該是老師, 有實際教學經驗的人才會知道怎麼樣編書, 我買過兩本作者寫的書 (Python 演算法), 風格也都是篇幅不大的精練之作.  

2023年6月19日 星期一

2023 年第 25 周記事

過去的一周可說非常充實, 周一周二完成老張同學賦予的 C 語言代課任務後, 週三開始著手幫公司撰寫新版維運軟體, 很早就想用 Python 改寫原先用 Javascript +WSH 撰寫的網頁版軟體, 每次遇到大量指令生成需求時就下一次決心, 但事過境遷就忘了, 因為技術世界有太多誘惑, 就像是愛情中的花花公子 (現代語叫渣男), 見一個很棒的新技術就見異思遷, 妄想學會天下武功, 但最終落得藝多不精慘況. 但這次無外務紛擾, 又已預見來自有司的壓力會愈來越大, 重新打造一個綜合資料處理與指令生成的工具迫在眉睫, 借助 ChatGPT 的幫助, 我上週三開工周五軟體就可用了, 這幾天剛好有任務馬上就派上用場, 但也發現幾個要改進之處, 總之, ChatGPT 讓我把原本要花一個月的工一周內就搞定 (剩下微調), 碼農真的一天都不能離開 ChatGPT 了. 

五月中芒果才套袋完畢, 本周就開始採收了, 週三晚上小舅打電話說白天時有跟小舅媽到我家去採, 各寄了一箱給小阿姨與左營阿姨, 說是去我家偷採的, 我說偷的比較好吃. 本週六補班 (端午節), 週日小舅媽拿鐵製長竿來幫我採, 她眼睛好, 隔著紙袋居然能看出芒果表皮已經有些微黃色, 採下來一打開紙袋果然可採收了, 失誤率奇低, 採了三大蔞只有兩三顆皮是青的. 週日早上去市集時提了一袋送給松尾伯母, 晚上出來高雄時載了兩大簍分享給同事與親朋好友 :




預計做如下分配 : 二姨媽 * 1 袋, 峰大師 * 1 袋, 琪大師 * 2 袋, 同事 * 2 袋, 大樓保全 * 3 袋, 仲仔 * 1 袋, 剩下較醜的留下來打冰沙用. 目前只採了大約 1/3, 端午連假回去繼續採, 阿運伯母與松盛伯母那邊還沒送, 一年中就只有六月的芒果與七月的龍眼有出產能拿得出來與眾親友分享, 這都是已作仙的母親大人遺澤. 

週日晚上出來高雄時, 高速公路開一半居然看到前面車子都停下來, 然後以走路速度前進, 我以為是有事故, 沒想到龜速到國三閘道才發現原來是道路施工, 平常開 50 分鐘就到家的居然花了一個半小時. 

2023年6月15日 星期四

MicroPython 學習筆記 : DHT22 溫溼度感測器測試

二哥嵌入式課程本周要驗收實作成果, 他上周要我幫他再買兩顆 DHT22 來測試 (奧松原廠晶片, 一片 145 元), 因為他去中將電子買的 DHT22 不太穩 (一片 220 元), 那塊 STM32F429 開發板有時讀不到溫濕度資料, 我今天用 D1 mini 的 MicroPython 測試每一片都 OK, 可見應該是 Driver 的關係. 

以下測試參考 Ranerd 的這篇 :


文中使用的 DHT22 模組為四隻腳, 但現在市面上的都是 +Vcc (左), GND (右), 以及 Data (中) 三隻腳, 以前要接的電阻現在應該已經整合到模組裡面去了. 原始程式使用 GPIO14 與 DHT22 中間的 Data 腳相接, 我改為 GPIO12. 另外於 while 無窮迴圈外加一層 try except 用來捕捉 Ctrl + C 鍵盤中斷事件, 這樣程式才不會停不下來 : 

from machine import Pin
from time import sleep
import dht 

sensor = dht.DHT22(Pin(12))
try:
    while True:
      try:
        sleep(2)
        sensor.measure()
        temp = sensor.temperature()
        hum = sensor.humidity()
        temp_f = temp * (9/5) + 32.0
        print('攝氏溫度: %3.1f C' %temp)
        print('華氏溫度: %3.1f F' %temp_f)
        print('濕度: %3.1f %%' %hum)
      except OSError as e:
        print('無法讀取 DHT22 輸出')
except KeyboardInterrupt:
    pass

結果可正常讀取溫濕度值 : 




執行中按 Ctrl+C 可隨時終止執行 :




參考 : 


2023年6月13日 星期二

2023 年第 24 周記事

最近因為颱風帶來雲雨帶, 這幾天豪雨不斷, 本想開車上班, 但想到塞車還是穿雨衣騎機車算了. 我週五晚上與二哥先回鄉下 (他帶畢業服回鄉下要跟阿公合照), 因週六邀大帥夫婦與仲仔, 峰大師來鄉下老家一聚 (連吳董也遠從湖內趕來), 由我下廚調製午餐, 但主菜之一的樹豆燉豬腳, 須先將樹豆浸一晚. 幸好週六放晴, 不過陰涼的天氣倒也暑熱大消. 

本周芒果開始零星採收, 有些是在欉黃自然掉落, 有些則是目視表皮泛紅黃色用長竿綁小布袋擰下來的, 通常都是太高無法套袋, 因陽光足而早熟. 估計下周高度較低有套袋的應可採收矣. 晚上接到小舅電話說他與舅媽今日下午有去我家採摘一些要寄給小阿姨, 我原計畫下周寄, 沒想到給小舅搶先了. 

忙了一個月餘, 今日終於完成代課任務, 總算鬆了一口氣, 老實說這差事耽誤我許多進度, 想說順便搞定 C++ 也沒全部達成, 不過 C 語言倒也又複習了一遍, 對接下來的 ESP32-CAM 專案有幫助. 只是領教了現在學生的課堂參與度, 覺得有點灰心, 不知是何原因 (真希望是因為我教得爛), 有機會想偷溜到台大電機系去旁聽, 觀察頂大課堂是何光景. 總之, 我又自由了! 以後絕不再接這種活. 

2023年6月12日 星期一

露天購買 DHT22 兩顆

上次幫二哥買的專題要用的 DHT22 似乎不穩定, 我昨天又上露天跟另一個賣家買兩顆 :





全家免運 290 元. 

C/C++ 學習筆記 : 檔案處理

檔案以儲存形式來分有純文字檔 (text file) 與二進位檔 (binary file) 兩種, 文字檔以字元編碼 (ASCII 或 Unicode) 方式儲存資料, 可直接在螢幕輸出閱讀 (但比較占空間), 例如副檔名為 .txt, .log, .json, 或 .csv 者皆為文字檔, 也是程式專案中最常使用的檔案格式. 

而二進位檔是將記憶體中的資料以原始未經過編碼的 byte 形式原封不動儲存至檔案中, 需透過特定軟體解碼才能使用或閱讀, 例如編譯過的程式檔, 圖片, 影像, 以及音樂等均以二進位檔方式儲存 (因為這樣比較節省空間), 副檔名為 .dat, ,pdf, 或 .exe 等皆為二進位檔.  

本篇測試使用免費的線上開發平台 replit.com 執行程式碼, 此平台提供 Python, C, Java, C# 等程式語言的開發環境, 註冊使用者可將程式碼儲存在平台上或透過連結分享給別人, 參考 :



一. C 的檔案處理 : 

C 語言是以資料串流 (IO Stream) 方式來處理記憶體與周邊儲存裝置 (例如磁碟) 的資料傳輸, 但基於存取的效率考量, 檔案處理時程式並非直接對磁碟進行存取, 而是透過在記憶體中劃出的一塊緩衝區 (buffer) 當作串流的中介, 程式存取的是緩衝區存放的資料, 因此將資料寫入檔案時並非直接寫入磁碟, 而是先寫入緩衝區, 等關閉檔案時才真正寫入磁碟. C 語言標準函式庫中的 stdio 函式庫負責檔案的開啟, 讀寫, 以及關閉, 也會自動處理緩衝區的配置, 因此若程式要用到檔案輸出入時, 必須用 include 匯入 stdio.h 標頭檔.   


1. 開啟與關閉文字檔 :    

C 語言以文字串流 (text stream) 方式, 如同水管中的水流般一個字元一個字元地循序處理. C 語言的標準函式庫 stdio 提供檔案開啟, 讀寫, 與關閉等操作, 並會自動處理緩衝區. 使用標準函式庫處理檔案時必須先匯入 stdio.h 標頭檔 :

#include <stdio.h>

使用此函式庫開啟檔案時會傳回一個 FILE 型態的指標, 它會指向所開啟檔案的緩衝區, 因此處理檔案時要先宣告一個 FILE 型態的指標 :

FILE *fp;   

stdio 的檔案開啟與關閉函式 API 如下 :


 stdio 函式 說明
 FILE *fopen(char *filename, char *mode) 以指定模式開啟檔案, 成功傳回檔案指標, 失敗傳回 NULL.
 int fclose(FILE *fp) 傳入檔案指標關閉該檔案, 成功傳回 0, 失敗傳回 EOF


檔案操作結束務必呼叫 fclose() 並傳入檔案指標關閉檔案, 此函式會先將緩衝區內的資料寫回檔案後緩衝區記憶體. 

fclose(fp);     

fopen() 第二個參數為開啟模式字串, 可用的模式如下表 : 


 模式字串 說明
 r 以唯讀模式開啟已存在之文字檔, 成功傳回檔案指標, 失敗 (檔案不存在) 傳回 NULL
 w 以覆蓋寫入模式開啟文字檔, 若檔案存在內容會先被清空, 若檔案不存在就建立檔案
 a 以附加寫入模式開啟文字檔 (新增的資料附加在原資料後面), 若檔案不存在就建立檔案
 r+ 以讀寫模式開啟已存在之文字檔, 成功傳回檔案指標, 失敗 (檔案不存在) 傳回 NULL
 w+ 以讀寫模式開啟文字檔, 若檔案存在內容會先被清空, 若檔案不存在就建立檔案
 a+ 以附加讀寫模式開啟文字檔 (新增的資料附加在原資料後面), 若檔案不存在就建立檔案
 rb+ 以讀寫模式開啟已存在二進檔, 成功傳回檔案指標, 失敗 (檔案不存在) 傳回 NULL
 wb+ 以讀寫模式開啟二進檔, 若檔案存在內容會先被清空, 若檔案不存在就建立檔案
 ab+ 以附加讀寫模式開啟二進檔 (新增的資料附加在原資料後面), 若檔案不存在就建立檔案


文字檔案的基本操作有讀取 (read only), 寫入 (write), 以及檔尾附加寫入 (append) 三種模式, 模式字串後面添加一個 "+" 表示可以更新檔案內容, 例如 r+ 與 w+ 都表示可讀寫檔案內容; 但 w+ 會先清除原來的內容, 而 r+ 則不會. 

fopen() 函式的第一參數為檔案名稱字串, 可以包含路徑, 在 Windows 系統下路徑之倒斜線必須用 \ 跳脫, 以下是絕對路徑的用法 : 

FILE *fp;                                           // 宣告指向檔案緩衝區的指標
fp=fopen("test.txt");                          // 開啟工作目錄下的 text.txt 檔 (目前目錄=C:\myfolder\)
fp=fopen("C:\\myfolder\\test.txt");   // 開啟指定目錄下的 text.txt 檔 
fp=fopen("C:/myfolder/test.txt");     // 開啟指定目錄下的 text.txt 檔

也可以使用相對路徑 :

fp=fopen("./myfolder/test.txt");     //目前目錄的子目錄 myfolder 下的 test.txt
fp=fopen("../myfolder/test.txt");    //目前目錄的上一層目錄 myfolder 下的 test.txt

fopen() 開啟檔案成功會傳回 FILE 型態的指標 (指向緩衝器位址), 開啟失敗則傳回 NULL, 個可利用傳回值判斷是否開檔成功, 例如 : 

if (fp==NULL) {
    printf("檔案開啟失敗\n");
    }
else {
    printf("檔案開啟成功\n");
    }

完整範例如下 :

/* 開啟和關閉文字檔案 test01.c */
#include <stdio.h>

int main() {
  FILE *fp;
  fp=fopen("helloworld.txt", "r");
  if (fp==NULL) {printf("檔案開啟失敗\n");}
  else {printf("檔案開啟成功\n");}
  fclose(fp);
  }

先在工作目錄下製作一個 helloworld.txt 檔案 (裡面有無內容均可), 執行程式結果會顯示檔案開啟成功; 然後用 rm 指令刪除 helloworld.txt 再次執行程式則顯示島案開啟失敗. 以下是在 replit.com 底下用 a.replit 執行的結果 : 




在 replit.com 執行 C 程式的方法參考下面這篇 :



2. 讀寫文字檔 :   

stdio 函式庫內建 fgets() 與 fputs() 函式來讀寫文字檔 : 


 讀寫文字檔的函式 說明
 char *fgets(char *str, int n, FILE *fp) 從指標 fp 讀取 n-1 個字元, 成功傳回 str 指標, 至檔尾傳回 NULL
 int fputs(char *str, FILE *fp) 將指標 str 字串寫入檔案 fp, 成功傳回非負整數, 失敗傳回 EOF


首先來測試用 fputs() 將字串寫入文字檔, 寫入文字檔可用 "w" 或 "a" 模式開檔, 用 "w" 模式開啟時若此檔案已存在, 則開啟成功後原本的內容會被清空; 若檔案不存在就建立一個空檔案, 總之用 "w" 模式建立的檔案, 其內容都是我們用 fputs() 等函式寫入的; 而 "a" 模式則是會保留原檔案內容 (如果檔案存在的話), 用 puts() 寫入的內容會附加在檔尾, 例如 :

/* 寫入文字檔 test02.c */
#include <stdio.h>

int main() {
  FILE *fp;
  char *str1="Hello\n";       //用指標儲存字串
  char str2[10]="World\n";    //用陣列儲存字串
  fp=fopen("helloworld.txt", "w");  //以 w 模式開啟文字檔
  fputs(str1, fp);   //將字串寫入文字檔
  fputs(str2, fp);   //將字串寫入文字檔
  printf("文字檔寫入完成\n");
  fclose(fp);
  }

執行結果如下 : 




可見執行寫入後, helloworld.txt 內容為兩列的 Hello 與 Wordl. 如果將上面程式中的兩個 fputs() 註解掉重新執行程式, 則 helloworld.txt 檔案內容將變成空白 (開檔後沒有寫入). 

接下來可以用 fgets() 函式來讀取上面範例程式產生的 helloworld.txt 內容, 用 fgets() 讀檔首先必須宣告一個字元陣列來儲存讀到的內容, 其長度與 fgets() 的第二參數相同 :

char str[50];    //長度與 fgets() 的第二參數相同

然後使用 while 迴圈從緩衝區中固定讀取若干字元存到字元指標所指之位址, 直到 fgets() 傳回 NULL 表示已讀到檔尾結束迴圈, 語法如下 :

while (fgets(str, 50, fp) != NULL) {  //每次讀取 50-1=49 個字元 (扣掉字串尾 \0)
    .......
    }

完整範例如下 : 

/* 讀取文字檔 test03.c */
#include <stdio.h>

int main() {
  FILE *fp;
  char str[50];  //讀取字串的暫存區, 長度與 fgets() 第二參數相同即可
  int count=0;   //統計讀了幾列
  fp=fopen("helloworld.txt", "r");
  if (fp != NULL) {
    printf("文字檔內容 :\n");
    while (fgets(str, 50, fp) != NULL) {
      printf("%s", str);  //印出讀取到的字串 (列)
      count++;
      }
    printf("讀取了 %d 列\n", count);
    fclose(fp);
    }
  else {printf("檔案開啟失敗\n");}
  }

執行結果如下 :




下列範例使用 fgets() 與 fputs() 來複製檔案 : 

/* 複製文字檔 test04.c */
#include <stdio.h>

int main() {
  FILE *sfp, *dfp;  //宣告來源與目的檔案指標
  char str[20];   //暫存 fgets() 從緩衝區讀取之字串
  dfp=fopen("helloworld2.txt", "w");  //用 w 模式開啟目的檔
  sfp=fopen("helloworld.txt", "r");   //用 r 模式開啟來源檔
  if (sfp != NULL) {
    printf("文字檔內容 :\n");
    while (fgets(str, 20, sfp) != NULL) {
      printf("%s\n", str);  //印出讀取到的字串 (列)
      fputs(str, dfp);  //將讀取之資料寫入目的檔緩衝區
      }
    fclose(sfp);   //關閉目的檔
    fclose(dfp);   //關閉目的檔
    }
  else {printf("檔案開啟失敗\n");}
  }

此例以上面的 helloworld.txt 為來源檔, 用 r 模式開啟檔案後傳回檔案指標 sfp; 以尚未建立的 helloworld2.txt 為目的檔, 用 w 模式開啟檔案後傳回檔案指標 dfp, 然後用 while 迴圈讀取來源檔 sfp 並將讀取之資料寫入目的檔直到檔尾 EOF 出現為止, 執行結果如下 :




可見原本不存在的 helloworld2.txt 在執行完程式後被建立了, 且內容與來源檔案 helloworld.txt 完全相同, 亦即完成了檔案複製動作. 


3. 格式化讀寫文字檔 :   

上面的範例中寫入檔案的均為字串, 而 fputs() 是專門用來將字串寫入檔案中的函式 (因為其第一參數為 char 類型), 如果要將整數用 fputs() 寫入檔案, 必須先用 stdio 函式庫的 sprintf() 函式將整數轉成字串, 其呼叫方式如下 : 

sprintf(字串變數, "%d", 整數變數);         //將整數變數轉為字串變數

如果要將浮點數轉成字串, 則可用 stdlib 函式庫的 gcvt() 函式, 呼叫方法如下 : 

char *字串變數=gcvt(浮點數變數, 總位數, 暫存字串變數);    //將浮點數變數轉為字串變數

注意, gcvt() 標頭檔定義於 stdlib, 須用 include 匯入 :

#include <stdlib.h>

例如 :

/* 用 sprintf() 死 gcvt() 將數值轉成字串 test05.c */
#include <stdio.h>
#include <stdlib.h>

int main() {
  FILE *fp;
  fp=fopen("out.txt", "w");
  int i_price=123;       //整數
  float f_price=123.45;  //浮點數
  char i_price_str[10];  //用來儲存轉成字串的整數
  char c[10];            //gcvt() 用來暫存字串的字元陣列
  sprintf(i_price_str, "%d", i_price);    //將整數轉成字串
  char *f_price_str=gcvt(f_price, 5, c);  //將浮點數轉成字串
  fputs(i_price_str, fp); //將整數字串存入檔案
  fputs("\n", fp);        //存入換行字元
  fputs(f_price_str, fp); //將浮點數字串存入檔案
  fclose(fp);
  }

執行結果如下 : 




讀進儲存在檔案中的數值字串時也必須透過 strtoi(), atoi(), atof() 等函式轉成數值. 

參考 : 


另一個較簡單的方法是使用 fprintf() 與 fscanf() 函式, 如同 printf() 與 scanf() 為對標準 IO (螢幕與鍵盤) 進行格式化輸出入, C 語言的 stdio 也有對於檔案 IO 的格式化輸出入函式 fprintf() 與 fscanf(), 不論是獨或寫, 可以透過格式化字元自動轉換類型 :


 格式化讀寫文字檔函式 欄位2
 int fprintf(fp, 格式化字串, 變數群)  依格式化字串輸出到檔案, 成功傳回輸出字元, 失敗傳回 EOF
 int fscanf(fp, 格式化字串, 變數群)  依格式化字串從檔案讀取, 成功傳回讀取字元, 失敗傳回 EOF


格式化字串用法與 printf() 及 scanf() 相同, 常用的是 %c (字元), %s (字串), %d (整數), %f (浮點數) 等. 完整範例如下 : 

/* 格式化讀寫文字檔 test06.c */
#include <stdio.h>

int main() {
  FILE *fp;
  char str[20];
  char name[20]="USB 讀卡機";  
  int count=10;  
  float price=21.5;
  fp=fopen("product.txt", "w");
  fprintf(fp, "品名: %s\n", name);
  fprintf(fp, "數量: %d\n", count);
  fprintf(fp, "價格: %f\n", price);
  fclose(fp);
  fp=fopen("product.txt", "r");
  if (fp != NULL) {
    printf("文字檔內容 :\n");
    while (fscanf(fp, "%s", str) != EOF) {
      printf("%s\n", str);  //印出讀取到的字串 (列)
      }
    fclose(fp);
    }
  else {printf("檔案開啟失敗\n");}
  }

執行結果如下 : 




可見不論是字串或數值的檔案讀寫, 用 fprintf() 與 fscanf() 的格式化字串就能搞定, 不需要用其他各種函式轉來轉去. 

2023年6月11日 星期日

用 Crucial 掃描電腦規格

 我在 2014 年 10 月 18 日向燦坤買的技嘉桌機 (因小狐狸們上國中寫作業要用) 至今仍可用, 幾年前移回鄉下當備用機, 有時筆電忘記帶充電線時還可應急繼續寫程式. 今年以來爾而開機都會進入 BIOS, 想必是主機板上面的 CR2032 沒電了, 前幾天在露天買模組時有順便買 CR2032. 

另外一個問題是此機記憶體僅 4GB, 性能不佳, 打算買中古記憶體來擴充, 但是不確定記憶體規格, 在控制台的系統只能查到 CPU 與記憶體容量, 無法查到主機板資訊 :




我找到 SSD 大廠 Crucial 提供的系統掃描器可以掃描出主機板規格 :






勾選我同意條款與條件後按 "開始免費掃描" 會下載一個程式 CrucialTWScan.exe, 我用 TotalVirus 掃描為 1/71, 看起來應該是安全軟體 :





執行此程式即開始掃描, 完成後會開啟一個網頁顯示結果 : 






可見主機板編號為 GIGABYTE GA-AM1M-S2P, 記憶體為 Kinston 4GB DDR3 1600, 系統分割為 100GB (硬碟為 500GB, C 碟 100GB, D 碟 400GB). 這樣就知道要找 DDR3 1600 的記憶體了.