สร้างแอป Todo สำหรับ Windows แบบเนทีฟด้วย C ล้วน (278KB, ไม่ใช้เฟรมเวิร์ก)
(github.com/Efeckc17)- Simple Todo เป็นแอป Todo สำหรับ Windows แบบเนทีฟที่สร้างด้วย C และ Win32 API เป็นโปรเจกต์ที่แสดงให้เห็นการเขียนโปรแกรม GUI บน Windows และความสามารถในการผสานกับระบบ
- ฟีเจอร์หลักคือการสร้าง·แก้ไข·ลบ Todo, ทำเครื่องหมายว่าเสร็จแล้ว, ย่อไปที่ system tray, ตัวเลือกเริ่มทำงานอัตโนมัติพร้อม Windows และการค้นหาแบบเรียลไทม์ที่ไม่แยกตัวพิมพ์ใหญ่-เล็ก
- ข้อมูลถูกบันทึกเป็น ไฟล์ไบนารี ที่
%APPDATA%\TodoApp\todos.datโดยเก็บรายการ Todo ทั้งหมดและรองรับ Todo ได้สูงสุด 100 รายการ - การ build ต้องใช้ MinGW-w64 และ Windows SDK บน Windows หลังจากโคลน repository แล้วคอมไพล์ด้วย
.\build.bat - โครงสร้างแบ่งเป็น
main.c,todo.c,gui.c,utils/search.cโดย GUI ใช้ Win32 API·Common Controls·UXTheme
ลักษณะของโปรเจกต์
- Simple Todo เป็นแอป Todo สำหรับ Windows แบบเนทีฟที่เขียนด้วย C และ Win32 API
- มุ่งสร้างไฟล์ executable แบบเนทีฟที่เบา และใช้ look and feel พื้นฐานของ Windows พร้อม visual style สมัยใหม่ของ Windows
- GUI อิงกับ Win32 API และรวมการผสานกับ system tray รวมถึงการคงอยู่ของข้อมูลผ่าน file I/O
ฟีเจอร์ Todo
- สามารถ สร้าง แก้ไข ลบ รายการ Todo ได้
- สามารถทำเครื่องหมายงานเป็นสถานะเสร็จแล้วได้
- สามารถตั้งค่า ลำดับความสำคัญ ให้แต่ละ Todo ได้
- ฟีเจอร์ค้นหาทำงานแบบเรียลไทม์และรองรับ การจับคู่แบบไม่แยกตัวพิมพ์ใหญ่-เล็ก
- สามารถย่อแอปไปไว้ที่ system tray ได้
- มีตัวเลือกให้รันอัตโนมัติเมื่อ Windows เริ่มทำงาน
วิธีจัดเก็บข้อมูล
- ข้อมูล Todo ถูกบันทึกไว้ที่
%APPDATA%\TodoApp\todos.dat - รูปแบบการบันทึกเป็น ไฟล์ไบนารี ที่เก็บรายการ Todo ทั้งหมด
- ความจุถูกจำกัดไว้สูงสุด 100 Todo
การ build และการรัน
- ข้อกำหนด
- Windows OS
- คอมไพเลอร์ GCC MinGW-w64
- Windows SDK ที่รวมมากับ MinGW
- ขั้นตอนการ build
- ติดตั้ง MinGW-w64 และเพิ่มไดเรกทอรี
binเข้าในPATHของระบบ - โคลน repository แล้วไปยังไดเรกทอรี
simple-todo-c - รัน
.\build.batเพื่อ build
- ติดตั้ง MinGW-w64 และเพิ่มไดเรกทอรี
- เริ่มรันด้วย
bin/todo.exe
องค์ประกอบการพัฒนา
- โครงสร้างโปรเจกต์แบ่งเป็นไฟล์ C ตามฟังก์ชัน
src/main.c: จุดเริ่มต้นของแอปพลิเคชันsrc/todo.c,src/todo.h: ลอจิกการจัดการ Todo และโครงสร้างข้อมูลsrc/gui.c: การพัฒนา GUIsrc/utils/search.c,src/utils/search.h: การพัฒนาและประกาศฟังก์ชันค้นหาsrc/app.manifest: manifest สำหรับ visual style ของ Windows
- องค์ประกอบหลักคือ Win32 API, Common Controls, UXTheme, File I/O
- สำหรับการดีบัก มีคู่มือ
DEBUGGING.mdที่ใช้ได้กับ Visual Studio, VS Code เป็นต้น
ไลเซนส์และการมีส่วนร่วม
- ไลเซนส์คือ MIT License
- รับการมีส่วนร่วมผ่าน Pull Request
3 ความคิดเห็น
มีความโรแมนติกดีนะ
ความคิดเห็นจาก Hacker News
การเขียนโปรแกรม GUI ด้วย Win32 มีบางอย่างที่ชวนให้ชอบอย่างประหลาด
มันอาจจะแปลกอยู่บ้าง แต่ถ้าอ่านบล็อกของ Raymond Chen จะเริ่มเห็นเหตุผล
Win32 API มีรากมาตั้งแต่ยุคโปรเซสเซอร์ 8088 จึงมีที่มาประมาณว่า ถ้าทำด้วยวิธีเฉพาะบางอย่างจะประหยัดโค้ดได้ 40 ไบต์ หรือใช้รีจิสเตอร์น้อยลงหนึ่งตัว
เมื่อก่อนเคยทำแอป GUI ของเล่นไว้เยอะด้วย mingw กับหนังสือของ Petzold และสิ่งต่าง ๆ อย่างการเขียน custom control, การวาดกราฟิกกับข้อความ, การจัดการสกรอลล์, hit testing ล้วนสนุกพอสมควร
เห็นในแอปใช้
strcpy,sprintfอยู่ ถ้าเป็นการเขียนโปรแกรมจริงจังควรใช้เวอร์ชันที่ตรวจสอบความยาวด้วย แปลกใจที่คอมไพเลอร์ไม่ได้พ่นคำเตือนออกมาเพียบใน Win32 API มีฟังก์ชันจำนวนมากที่ใช้แทนสิ่งที่อยู่ในไลบรารีมาตรฐาน C ได้ ถ้าอยากลดขนาดไฟล์ executable จริง ๆ ก็ลองเขียนแอปโดยไม่ใช้ไลบรารีมาตรฐาน C ได้ มี
ZeroMemory()แทนmemset()และCopyMemory()แทนmemcpy()ถึงจุดหนึ่ง การเขียนโค้ด C ล้วน ๆ เองจะเริ่มทรมาน แต่คิดว่าการลองทำด้วย C ล้วน ๆ ในช่วงแรก ๆ เป็นวิธีเรียนรู้ที่ดีที่สุด การจัดการรายละเอียดเองทำให้ระหว่างเรียนมองเห็นภาพได้ดีว่าเบื้องหลังเกิดอะไรขึ้น
ถ้าอยากลองเขียน Win32 GUI ต่อ แนะนำให้ดู WTL(Windows Template Library) เป็น C++ wrapper ครอบ Win32 API ทำให้เข้าใจพฤติกรรมได้ง่ายขึ้นมาก
แม้จะมีฟีเจอร์เยอะมาก แต่ด้วยเหตุนี้ขนาดจึงมีแค่ประมาณ 3MB ไม่มีเฟรมเวิร์ก และมีเพียงไฟล์ executable เดียว ถ้าซอฟต์แวร์ทั้งหมดยังเป็นแบบนี้ก็คงดี
strncpyแทนstrcpyก็ต้องยอมรับว่าทั้งคนและแม้แต่ เครื่องมือ AI จะคอยบอกซ้ำ ๆ ว่าไม่ควรทำแบบนั้นโดยส่วนตัว เหตุผลหลักอย่างหนึ่งที่ใช้ Zig คือมันช่วยลดกับดักที่พบบ่อยเหล่านี้ได้มาก แน่นอนว่า C ก็ยังโอเค
ZeroMemory()แทนmemset()และCopyMemory()แทนmemcpy()แต่ MSVC intrinsic น่าจะใช้คำสั่งrep stos/movsซึ่งอาจเล็กกว่าการเรียกฟังก์ชัน เพราะการเรียกฟังก์ชันยังรวมขนาดของ entry ใน import table ด้วย
fopen/fread/fcloseด้วย แต่ดูเหมือนไม่ได้ใช้ Win32 API อย่างCreateFile/ReadFile/WriteFileแทนที่จะลำบากเรียก
CreateWindow()ทีละคอนโทรล ตามธรรมเนียมแล้วจะจัดวาง dialog resource ไว้ในไฟล์.rc(Visual Studio ยังมีตัวแก้ไข dialog แบบมองเห็นภาพสำหรับทำสิ่งนี้อยู่) แล้วใช้CreateDialog()แทนCreateWindow()วิธีนี้จะสร้างคอนโทรลทั้งหมดให้อัตโนมัติ ถ้าเพิ่ม application manifest ก็จะได้สไตล์ UI สมัยใหม่และการรองรับ high-DPI ด้วย
แต่ถ้าต้องการให้ปรับขนาดได้ ก็ยังต้องจัดการเองอยู่ดี ทว่าโดยทั่วไปทำได้ง่ายและโค้ดก็ไม่เกินไม่กี่ร้อยไบต์
มี K&R C อยู่ก็จริง แต่ระบบปฏิบัติการแต่ละตัวนอก UNIX ต่างก็เลือกฟังก์ชันที่จะให้เองตามใจ
อีกทั้งในที่ที่ไม่ใช่ตระกูล UNIX ยังมีธรรมเนียมว่าไลบรารีมาตรฐานเป็นความรับผิดชอบของคอมไพเลอร์ C ไม่ใช่แพลตฟอร์ม
ดังนั้นไลบรารี C จึงถูกจัดให้โดย Borland, Watcom, Symantec, Microsoft, Green Hills, Zortech ฯลฯ
Mac OS ก็เป็นแบบเดียวกันจนกว่าจะมี MPW และ IBM กับ Unisys, ไมโครคอมพิวเตอร์กับเมนเฟรม, รวมถึง VMS ก่อน OpenVMS ก็เช่นกัน
ตั้งแต่ Windows 10 เป็นต้นมาก็มี Universal C Runtime ด้วย
ไม่ต้องใช้ resource compiler และไม่ต้องใช้ linker เพื่อสร้าง resource DLL
ไฟล์ resource กับรูปแบบไบนารีของมันไม่ใช่ API ที่ดี
ถ้ามีการเรียก
CreateWindowในภาษาระดับสูงพอสมควร ก็น่าจะใช้เมตาโปรแกรมมิงสร้าง DSL คล้าย resource ที่กลมกลืนกับภาษาได้ดีเมื่อก่อนเคยทำของคล้าย ๆ กันบน Linux ด้วยแอสเซมบลีให้มีขนาด ต่ำกว่า 2KiB: https://gaultier.github.io/blog/x11_x64.html
อย่างที่คนอื่นพูดกัน ถ้าทำด้วย C ล้วนและใช้ dynamic linking อย่างน้อยบน Linux ก็ทำให้ต่ำกว่า 20KiB ได้ง่าย Windows มีสิ่งที่รวมมากับระบบปฏิบัติการเป็นพื้นฐานมากกว่ามาก อาจจะยิ่งง่ายกว่า
ไม่ว่าอย่างไร ก็ขอเอาใจช่วยความพยายามนี้ ลองใช้ตัวเลือกการลิงก์ที่ผมเขียนไว้ท้ายบทความของผม น่าจะช่วยลดขนาดได้
ยังไม่ได้ลอง build ncurses ด้วย musl
พอเห็นว่า “รูปลักษณ์และความรู้สึกแบบ Windows เนทีฟ” ก่อนรันจริงก็เลยคาดหวังว่าใน list view จะมี context menu และเมื่อดับเบิลคลิกฟิลด์บางช่อง จะมีเมนูดรอปดาวน์หรือกล่องข้อความแบบใกล้เคียง inline โผล่ขึ้นมา
ตอนนี้ไม่รู้แล้วว่าเป็นเพราะคนไม่รู้วิธีออกแบบโปรแกรมให้เข้ากับ Win32 UI/UX หรือเป็นเพราะผมแก่เกินไป
พูดว่า “ไม่มีเฟรมเวิร์ก” แต่พอใช้ DPI scaling แล้วฟอนต์เบลอ, ไม่มี การรองรับ Tab, ในช่องข้อความกด Ctrl-A เพื่อเลือกทั้งหมดก็ไม่ได้, ฟีเจอร์หลายอย่างที่แม้แต่เฟรมเวิร์กยุคก่อนสมัยใหม่ยังมีให้ก็ขาดไป และยังมีข้อผิดพลาดตอนเพิ่มแถวด้วย
ไม่รู้ว่า “สมัยใหม่” หมายถึงอะไรกันแน่
โค้ดนี้จะตรวจสอบแบบไดนามิกว่าใน
user32:SetProcessDpiAwarenessContext,shcore:SetProcessDpiAwareness,user32:SetProcessDPIAwareอันไหนใช้งานได้ แล้วเรียกใช้อันนั้นถ้าเวอร์ชัน Windows เก่ามากจนไม่มีอันไหนถูก implement เลย (ก่อน Windows XP) ก็จะไม่เรียกอะไร
ฟีเจอร์ส่วนใหญ่ที่บอกว่าขาด โดยเฉพาะการย้ายโฟกัสด้วย Tab ระหว่างคอนโทรล เพิ่มได้ง่ายมาก
โปรแกรมเมอร์ 6502 ในตัวผมกำลังตายอยู่ข้างในเมื่อเห็นว่า 278KB เดี๋ยวนี้ถือว่าเบาแล้ว
อุปสรรคแรกที่เจอคือถ้า git ตั้งค่า
core.autocrlf=falseไฟล์build.batจะไม่ทำงาน พอเปลี่ยนเป็นcore.autocrlf=trueแล้ว clone ใหม่ก็ build ได้toolchain ที่ใช้คือ
x86_64-15.1.0-release-win32-seh-msvcrt-rt_v12-rev0.7zจาก https://github.com/niXman/mingw-builds-binaries/releases/tag...กรณีนี้ได้ไฟล์
.exeขนาด 102KB ซึ่งดีกว่าตั้งแต่เริ่มเมื่อเทียบกับ 278KB ที่อ้างไว้ ผู้เขียนอาจใช้ toolchain หรือการตั้งค่าอื่นก็ได้ เลยอยากให้มีขั้นตอน reproduce ที่แน่ชัดถ้าส่งสวิตช์บางตัวให้ GCC ก็ลดได้อีก
gcc -Os=> 100KBgcc -Oz=> 99KBgcc -flto=> 101KBgcc -s=> 51KBgcc -s -Oz -flto=> 47KBถ้าสนใจแค่ขนาด
.exeเล็ก ๆ ก็ยังมีพื้นที่ให้ปรับปรุงอีกมากถ้าไม่มีข้อมูล stack trace, โครงสร้างพื้นฐานของ dynamic linking, ตาราง exception handling อะไรพวกนี้ ก็น่าจะเบากว่านี้มาก ตาราง exception handling จำเป็นแม้ใน C เพราะ exception อาจวิ่งผ่านฟังก์ชัน C ได้
จำได้ว่าเมื่อก่อนเคยเขียนอะไรแบบนี้แล้วมันเล็กกว่านี้ เป็นเพราะ MinGW หรือเปล่า?
น่าจะเป็นปฏิกิริยาต่อกระแสขนาดดาวน์โหลดของ shareware ที่ใหญ่ขึ้น เป็นยุคมืดของ MFC
ผมจำมันได้ว่าเป็นเสียงเฮครั้งสุดท้ายของ assembly ที่ไม่ใช่ retrocomputing ควบคู่กับการเขียนโปรแกรม Palm Pilot 68k ยุคแรก ๆ
เพื่อน ๆ ก็แค่ลองทำดูและทำแอปนี้ขึ้นมาเล่น ๆ ความเห็นทั้งหลายก็ถูกแล้ว ของแบบนี้น่าจะทำด้วย C++ หรือภาษาอื่นได้สมเหตุสมผลกว่า
ไม่แน่ใจว่าทำไม แต่เหมือนเอกสาร Windows API จะแนะนำให้เขียนโค้ดสไตล์ C ด้วยคอมไพเลอร์ C++ ดูเหมือน Microsoft มองว่า C++ เป็น superset ที่ปรับปรุงจาก C และเห็นว่าควรใช้แม้กับโค้ดสไตล์ C
ถ้าจะใช้ Win32 API ภาษาอื่นก็ไม่ได้เปลี่ยนอะไรโดยพื้นฐาน แถมอาจทำให้งงกว่าเดิมด้วย
คนมักติดกรอบคิดแบบ C++ และสำหรับคนที่ไม่คุ้นกับ Win32 API มันคงทำให้ภาพรวมกลายเป็นก้อนที่สับสนยิ่งขึ้น
เป็นความพยายามที่น่ารักมาก และไม่ว่าคนอื่นจะคิดอย่างไร ความคุ้นเคยกับ Win32 API ก็เป็นทักษะพื้นฐานที่ใช้ได้ดี
เห็นหลายคนที่ทำซอฟต์แวร์หรือเว็บไซต์ซึ่งโหลด JS หรือ C# หลาย MB เพื่อจะส่ง telemetry 278KB ทุกครั้งที่ผู้ใช้ขยับเมาส์ มาประชดประชันกันอยู่แถวนี้
แอปนี้ใช้ RAM 1.5MB และทั้งคู่เปิดขึ้นมาทันที
เวลาพัฒนาแอป Windows การที่ ทำทุกอย่างด้วย C ได้ ทำให้รู้สึกดีเสมอ
วิธีสร้างขึ้นมาจากองค์ประกอบพื้นฐานระดับต่ำสุดมันให้ความรู้สึกเป็นธรรมชาติ
ในทางกลับกัน บน macOS ทุกอย่างเป็นออบเจกต์ Objective-C ดังนั้นแม้จะเขียนแอปด้วย C ล้วนได้ แต่ก็เหมือนลงไปต่ำกว่าชั้น abstraction ที่ Apple ตั้งใจไว้หนึ่งระดับ ทำให้เยิ่นเย้อขึ้นแทบจะ 1000 เท่า ถ้าจะทำอะไรสักอย่าง แทบจะต้องเชิดลำดับชั้นคลาสของ Objective-C เหมือนหุ่นเชิด ซึ่งรู้สึกไม่สบายใจเอามากๆ
ไม่เข้าใจว่าทำไมถึงเอา API แบบคลาสของ Objective-C ไปวางซ้อนบน API เชิงกระบวนการพื้นฐานแบบ Win32 อีกทีไม่ได้ ในทางเทคนิค Win32 ก็ “อิงคลาส” อยู่เหมือนกัน แต่เป็นแบบขั้นต่ำ
เลยยังไม่ค่อยรู้สึกอยากพอร์ตโค้ด C ของผมไป macOS ในเร็วๆ นี้
แต่คอมไพเลอร์ MIDL สำหรับอินเทอร์เฟซ COM จะสร้างเฮดเดอร์ที่ทำให้ใช้และ implement อินเทอร์เฟซใน C ได้: https://www.codeproject.com/KB/com/com_in_c1.aspx
เท่าที่ผมรู้ ทุกวันนี้ยังใช้ได้กับ WinRT อยู่ด้วย เพียงแต่โครงสร้าง C และชื่อฟังก์ชันที่สร้างขึ้นมาต้องใส่ namespace เต็มๆ ทำให้หน้าตาน่าเกลียดมาก: https://stackoverflow.com/a/7437006/111335
แทบจะเป็นตัวแทนแบบ drop-in ของ Macintosh Toolbox API ที่
user32.dllลอกมาอย่างโจ่งแจ้งแต่ถ้าไม่นับความรู้สึกอบอุ่นในฐานะโปรแกรมเมอร์แล้ว UI ต้องการแนวคิดเชิงวัตถุจริงๆ ทรีของออบเจกต์ต่างชนิดที่แชร์พฤติกรรมร่วมกันบางส่วน อธิบายโค้ด UI ได้แทบจะ 99% อยู่แล้ว
Toolbox กับ Carbon ขาดความเป็นเชิงวัตถุแบบนั้น เลยฝืนอยู่พอสมควร ความคิดสร้างสรรค์อย่างหนึ่งที่ Windows เพิ่มเข้ามาจริงๆ ก็คือ window class
ส่วนตัวแล้ว ความรู้สึกแปลกๆ เวลาจัดการคลาส Objective-C โดยตรงจาก C นั้น ผมรู้สึกแบบเดียวกันทุกครั้งที่ต้องกำหนด window class หรือ window procedure ในโค้ด
user32.dll[0]เช่นเดียวกับที่ UI ต้องการเชิงวัตถุ เชิงวัตถุก็ต้องการฟีเจอร์ภาษาเฉพาะทาง ถึงจะสร้างได้แม้ไม่มี แต่ความใช้งานง่ายจะแย่ลง 2000 เท่า
[0] หรืออะไรก็ตามที่เกี่ยวกับ GTK/GObject
ดูเหมือนกำลังลิงก์กับไลบรารีแบบสแตติกอยู่
ควร ลิงก์กับ DLL ไม่ใช่ไลบรารีแบบสแตติก แบบนั้นขนาดแอปพลิเคชันจะลดลงมาก
ถ้ายังไงก็ต้องแจก DLL ไปพร้อมกับโปรแกรมอยู่แล้ว (เพราะไม่ใช่ส่วนหนึ่งของระบบปฏิบัติการ) ใน DLL ก็มีฟังก์ชันทั้งหมดอยู่ และ DLL แต่ละตัวอาจมี C runtime ของตัวเองด้วย
ถ้าคอมไพล์ทุกอย่างแบบสแตติกเป็น EXE ไฟล์เดียว ก็มี C runtime แค่ชุดเดียว และตัดฟังก์ชันที่ไม่ได้ใช้ออกได้ง่าย
DLL จะช่วยลดขนาดได้ก็ต่อเมื่อโค้ดนั้นถูกแชร์ระหว่างหลายโปรแกรมเท่านั้น
ถ้าลิงก์แบบไดนามิกกับ
MSVCRxx/VCRUNTIMEผู้ใช้ต้องดาวน์โหลด DLL ตัวนั้นเวอร์ชันที่ตรงเป๊ะจาก Microsoftถ้าลิงก์แบบไดนามิกกับ
MSVCRTก็ไม่มีปัญหานั้น แต่ทำใน Visual Studio ได้ยากมากกรณีที่ไม่ควรลิงก์ CRT แบบสแตติกจริงๆ น่าจะมีแค่เรื่องการปฏิบัติตาม LGPL ประมาณนั้น
พี่ๆ ลมหายใจอุ่นๆ เหมือนส่งมาถึงตรงนี้เลย...