พัฒนาแอป Qt ให้สลับธีมตามระบบปฏิบัติการได้อัตโนมัติ
บทเรียนจากการพัฒนา Packet Sender เมื่อการฟังค่าธีมจาก OS ไม่ใช่เรื่องของ Background Compute แต่เป็นเรื่องของ GUI-State

ภาพประกอบจากคลังภาพสต็อก ไม่ใช่ภาพจากเหตุการณ์จริง
- Packet Sender เดิมต้องรีสตาร์ทแอปจึงจะเปลี่ยนธีมตาม OS ได้
- การใช้ Background Thread (QThread) ตรวจจับธีมบน macOS ทำให้แอปพัง (Crash)
- ทางออกที่เสถียรคือการใช้ QTimer บน Main Thread เพื่อเลี่ยงการแย่งทรัพยากร
จุดเริ่มต้นของงานนี้มาจากการทดสอบโหมดสว่าง (Light Mode) ให้กับแอป Packet Sender ซึ่งสร้างและดูแลโดย Dan Nagle เนื่องจาก Dan ใช้โหมดมืด (Dark Mode) เป็นหลัก เขาจึงมองข้ามจุดที่ว่ายังมีบางส่วนของธีมมืดค้างอยู่เมื่อแอปสลับมาเป็นโหมดสว่าง งานแรกที่ผู้เขียนได้รับมอบหมายจึงเป็นการทดสอบส่วนติดต่อผู้ใช้ (UI) แบบแมนนวลอย่างละเอียด เพื่อให้มั่นใจว่าการเปลี่ยนจากโหมดมืดเป็นโหมดสว่างทำงานได้อย่างสมบูรณ์ 100%
ก่อนหน้านี้ผู้เขียนเข้าใจว่า Packet Sender ทำงานตามการตั้งค่าของระบบปฏิบัติการมาโดยตลอด (โดยใช้ macOS เป็นหลัก) แต่ความจริงคือการจะเปลี่ยนโหมดในตอนนั้น ผู้ใช้งานจะต้องรีสตาร์ทแอป Packet Sender เสียก่อน เมื่อเปิดขึ้นมาใหม่ แอปจึงจะอ่านค่าการตั้งค่าจาก OS และปรับธีมที่ถูกต้องให้ แต่สิ่งที่แอปยังทำไม่ได้คือการอัปเดตตัวเองแบบเรียลไทม์เมื่อ OS เปลี่ยนธีม เช่น กรณีที่ผู้ตั้งค่าเครื่องให้สลับโหมดสว่างและมืดอัตโนมัติตามช่วงเวลาของวัน

ภาพประกอบจากคลังภาพสต็อก ไม่ใช่ภาพจากเหตุการณ์จริง
ผู้เขียนเชื่อว่าโครงสร้างพื้นฐานของระบบมีพร้อมอยู่แล้ว ขาดเพียงแค่การทำให้แอปรับรู้ว่า OS ได้เปลี่ยนโหมดไปแล้วเท่านั้น ในการแก้ปัญหานี้ Dan ได้เล่าให้ฟังทางโทรศัพท์ว่าเขาเคยแก้ปัญหาลักษณะเดียวกันนี้ในแอป Qt ตัวอื่นสำหรับลูกค้า โดยใช้ Thread ในการช่วยจัดการ ผู้เขียนจึงนำแนวคิดนี้ไปปรึกษา AI อย่าง Grok เพื่อขอชุดคำสั่งในการใช้ QThread แต่ผลลัพธ์ที่ได้กลับทำให้แอปเกิดการพัง (Crash) อย่างหนัก
ในทางเทคนิคแล้ว การฟังค่าการเปลี่ยนโหมด Light/Dark ไม่ใช่ปัญหาเรื่องการประมวลผลเบื้องหลัง (Background-compute) แต่เป็นปัญหาเกี่ยวกับสถานะของ GUI (GUI-state problem) ธีมของระบบปฏิบัติการจะถูกแชร์ร่วมกับ AppKit และ Widget ทุกตัว การใช้ Thread เสริมมาอ่านค่ารูปลักษณ์แล้วสั่ง setPalette() จะเกิดการแย่งชิงทรัพยากรกับ Toolkit ซึ่งบน macOS ความขัดแย้งนี้ส่งผลให้เกิดการทำงานที่ไม่ได้กำหนดไว้และทำนายไม่ได้ จนนำไปสู่ Hard Crash ในที่สุด
ทางออกที่ผู้เขียนเลือกใช้ในท้ายที่สุดคือการเปลี่ยนมาใช้ QTimer แทน โดย QTimer ที่ผูกกับ qApp จะทำงานอยู่บน Main Event Loop ทำให้ทุกๆ ติ๊กของการทำงานรันอยู่บน Thread เดียวกันกับที่ควบคุม Widget การตรวจสอบว่าตอนนี้เป็นโหมดมืดหรือไม่ และการสั่ง applyTheme() จึงถูกจัดลำดับขั้นตอน (Serialized) ไปพร้อมกับการวาดภาพ (Paint) การปรับขนาด (Resize) และอีเวนต์เกี่ยวกับธีมอื่นๆ โดยไม่มี Thread ตัวที่สองเข้ามาแทรกแซงบน QPalette และยังคงอยู่ภายใต้ Thread ที่ AppKit อนุญาตบน macOS
แม้ว่าการทำ Polling ทุกๆ สองสามวินาทีอาจจะดูไม่ใช่แนวทางที่หรูหราที่สุด เพราะใน Qt 6.5 ขึ้นไปจะมีฟังก์ชันอย่าง QStyleHints::colorSchemeChanged() และ Widget ต่างๆ ก็รองรับ QEvent::ApplicationPaletteChange อยู่แล้ว แต่การทำ Polling ก็ยังคงเป็นวิธีที่ถูกต้องในแง่ของการไม่ข้ามขอบเขตของ Thread (Thread Boundary) ข้อเสนอเรื่อง Thread ของ Dan อาจจะช่วยให้รู้ว่า OS เปลี่ยน แต่ไม่ได้ช่วยให้สามารถนำการเปลี่ยนแปลงนั้นไปใช้ได้อย่างถูกต้องตามกฎของระบบ
ที่มา: Dev.to
พบข้อมูลผิดพลาดในบทความนี้? แจ้งปัญหาบทความนี้
ความคิดเห็น
แสดงความคิดเห็น