คลิปบอร์ดกับลากและวางทำงานอย่างไร — จัดการการถ่ายโอนข้อมูล OLE ให้ถูกต้องในแอปธุรกิจ
· Go Komura · Windows, คลิปบอร์ด, ลากและวาง, OLE, COM, การพัฒนา Windows, WinForms, WPF
“เมื่อเราวางตารางที่คัดลอกจาก Excel การจัดรูปแบบพัง เราอยากให้มันวางเป็นตาราง” “เนื้อหาที่เราคัดลอกในแอปกลายเป็นสิ่งแปลกเมื่อเราวางลงใน Word” “เราอยากรับไฟล์ด้วยการลากและวางได้” — ในบทสนทนาให้คำปรึกษาเรื่องการเปลี่ยนแอปธุรกิจ คำขอรอบคัดลอกและวางกับลากและวาง (D&D) เป็นของประจำ
เพราะสิ่งเหล่านี้เป็น “ฟีเจอร์ที่ทุกคนถือเป็นเรื่องธรรมดา” พอดี ว่าพวกมันทำงานอย่างไรจริง ๆ จึงถูกรู้น้อยอย่างน่าแปลก หากคุณคิดคลิปบอร์ดเป็น “กล่องที่คุณใส่ข้อมูลชิ้นเดียว” คุณอธิบายไม่ได้ว่าทำไมสำเนาเดียวกันจึงให้ผลต่างกันขึ้นกับที่วาง หรือทำไมการวางหยุดทำงานหลังปิดแอปต้นทาง คลิปบอร์ดจริงคือกลไกที่วางเนื้อหาเดียวกันในหลายรูปแบบพร้อมกัน แล้วให้ฝั่งวางเลือกรูปแบบที่เข้าใจ
และลากและวาง ที่ก้น คือการถ่ายโอนข้อมูล OLE ที่ส่งผ่านการแทนข้อมูลเดียวกันพอดีกับคลิปบอร์ด (IDataObject) ผ่านอินเทอร์เฟซ COM กล่าวอีกนัย คัดลอกและวางกับ D&D เป็นพี่น้อง: เข้าใจอันหนึ่งถูกต้อง อีกอันอยู่ที่นั่นพอดี
บทความนี้มุ่งไปที่เจ้าหน้าที่ไอทีในบริษัทขนาดเล็กและกลาง และนักพัฒนาแอป Windows มันผูกเข้าด้วยกันในภาพเดียว ว่ารูปแบบคลิปบอร์ดทำงานอย่างไร แนวปฏิบัติฝั่งวางและฝั่งคัดลอก วิธีเฝ้าคลิปบอร์ดที่ถูกต้อง นโยบายการบริหารสำหรับประวัติคลิปบอร์ด การซิงค์คลาวด์ และ RDP และโครงสร้างกับกับดักของลากและวางแบบ OLE
1. สรุปก่อนเลย
- คลิปบอร์ดคือพื้นที่เดียวที่แอปบนเดสก์ท็อปเดียวกัน (window station) ใช้ร่วมกัน และสิ่งที่นั่งอยู่ที่นั่นไม่ใช่ “ข้อมูลชิ้นเดียว” แต่เป็นเนื้อหาเดียวกันในหลายรูปแบบพร้อมกัน เซสชันอื่น เช่น RDP มีคลิปบอร์ดต่างกันแต่เดิม ฟีเจอร์การเปลี่ยนทิศทางคือสิ่งที่เชื่อมทั้งสอง เพราะปลายทางเลือกรูปแบบที่เข้าใจ สำเนาเดียวกันจึงให้ผลต่างกันขึ้นกับที่วาง12
- สำหรับข้อความ ใช้ CF_UNICODETEXT CF_TEXT เป็น ANSI และขึ้นกับโค้ดเพจ และบนระบบญี่ปุ่นมันเป็นแหล่งเพาะอักขระเพี้ยน ระบบแปลงระหว่างทั้งสองโดยนัย แต่ฝั่งที่เป็นต้นแบบคือยูนิโค้ด3
- ไฟล์เดินทางเป็น CF_HDROP (อาร์เรย์ของพาธที่จบด้วย NUL คู่) และข้อความที่จัดรูปแบบใช้รูปแบบที่ลงทะเบียน “HTML Format” HTML Format มีโครงสร้างแปลก: ข้อความ UTF-8 พร้อมส่วนหัวของออฟเซ็ตไบต์45
- สาเหตุจริงของ “ฉันปิดแอปต้นทางแล้ววางไม่ได้อีก” คือ delayed rendering มันคือกลไกที่วางไม่ใช่เพย์โหลด แต่เพียงคำสัญญาว่าจะ “ผลิตเมื่อถูกขอ” หากคุณข้ามการทำให้เป็นรูปเป็นร่างตอนออก (การตอบ WM_RENDERALLFORMATS หรือ OleFlushClipboard สำหรับ OLE) การวางหยุดทำงาน26
- ถือข้อมูลที่วางเป็นอินพุตที่ไม่น่าเชื่อถือจากภายนอก Microsoft เองระบุตรง ๆ ว่า “ข้อมูลคลิปบอร์ดไม่น่าเชื่อถือ จงแยกวิเคราะห์อย่างระมัดระวัง”7
- สำหรับการเฝ้าคลิปบอร์ด AddClipboardFormatListener + WM_CLIPBOARDUPDATE คือตัวเลือกเดียว อย่าใช้การโพล และอย่าใช้ SetClipboardViewer เก่า (โซ่ผู้ชม) รูปแบบที่ลงทะเบียนที่กันความลับออกจากประวัติและการซิงค์ (ExcludeClipboardContentFromMonitorProcessing และพวก) ก็ถูกจัดให้เช่นกัน81
- ประวัติคลิปบอร์ด (Win+V) และการซิงค์คลาวด์เป็นเรื่องการจัดการไอที คุณควบคุมได้ด้วย AllowClipboardHistory และ AllowCrossDeviceClipboard ผ่าน GPO / Intune (Policy CSP) และการเปลี่ยนทิศทางคลิปบอร์ดของ RDP มีนโยบายเฉพาะของตนเอง91011
- ลากและวางคือ COM IDataObject เดียวกันกับคลิปบอร์ดถูกส่งระหว่าง IDropSource (ต้นทางลาก) กับ IDropTarget (เป้าหมายวาง) ผ่านลูป DoDragDrop RegisterDragDrop ต้องการการเริ่มต้นด้วย OleInitialize (STA)1213
- คุณวางจาก File Explorer สิทธิ์ธรรมดาลงบนแอปที่ยกระดับไม่ได้ UIPI (การบล็อกข้อความตามระดับความสมบูรณ์) คือสาเหตุ และเป็นข้อจำกัดที่คุณควรรู้ตอนออกแบบ14
ด้านล่างเราเดินผ่านสิ่งนี้จากรากฐานคลิปบอร์ดขึ้นไป
2. คลิปบอร์ดคืออะไรจริง ๆ — ไม่ใช่ “ข้อมูลชิ้นเดียว” แต่เป็น “เนื้อหาเดียวกันในหลายรูปแบบ”
คลิปบอร์ดคือกลไกแบ่งปันข้อมูลร่วมที่ทุกแอปที่แชร์เดสก์ท็อปเดียวกันเข้าถึงได้ (ให้แม่นยำกว่า มันเป็นราย window station: เซสชันผู้ใช้ต่างกันหรือเซสชัน RDP แต่ละอันมีคลิปบอร์ดของตนเอง คัดลอกและวางทำงานข้าม RDP เพราะฟีเจอร์การเปลี่ยนทิศทางเชื่อมทั้งสอง — บทที่ 7) หลักการแรกคือมันขับด้วยผู้ใช้: ท่าทางการออกแบบอย่างเป็นทางการคือคุณไม่ใส่ข้อมูลเข้าหรือดึงข้อมูลออกลับหลังผู้ใช้1
จุดสำคัญคือการคัดลอกไม่วาง “ข้อมูลชิ้นเดียว” หน้าต่างที่คัดลอกเทคลิปบอร์ดแล้ววางหลายรูปแบบเรียงกัน แสดงเนื้อหาเดียวกันจากรูปแบบที่ทำได้มากกว่าลงไปยังรูปแบบที่ทำได้น้อยกว่า2 ตัวอย่างเช่น เมื่อคุณคัดลอกตารางในสเปรดชีต ในเชิงแนวคิดสิ่งต่อไปนี้อยู่บนคลิปบอร์ดพร้อมกัน
| ลำดับความสำคัญ | รูปแบบ | เนื้อหา |
|---|---|---|
| 1 | รูปแบบส่วนตัวของแอป | การแทนภายในที่สมบูรณ์ รวมสูตรและการจัดรูปแบบ (สำหรับวางกลับเข้าแอปเดียวกัน) |
| 2 | HTML Format | ชิ้น HTML ที่คงโครงสร้างตารางและการจัดรูปแบบ |
| 3 | CSV | ข้อความที่คั่นด้วยเซลล์ |
| 4 | CF_UNICODETEXT | ข้อความธรรมดาที่คั่นด้วยแท็บ |
| 5 | รูปแบบภาพ | บิตแมปของลักษณะตาราง |
ฝั่งวางเลือกรูปแบบที่เข้าใจจากรายการนี้ แล้วดึงออก วางลงใน Word แล้วคุณได้ตารางที่จัดรูปแบบ วางลงใน Notepad แล้วคุณได้ข้อความที่คั่นด้วยแท็บ — เพราะทั้งสองเลือกรูปแบบต่างกัน “ผลขึ้นกับที่วาง” ไม่ใช่บั๊ก แต่เป็นผลปกติของการออกแบบนี้
flowchart TB
accTitle: ทำไมสำเนาเดียวกันจึงให้ผลต่างกันขึ้นกับที่วาง
accDescr: ฝั่งคัดลอกวางเนื้อหาเดียวกันบนคลิปบอร์ดในหลายรูปแบบ และฝั่งวางเลือกรูปแบบที่เข้าใจ ดังนั้น Word ได้ตารางที่จัดรูปแบบ และ Notepad ได้ข้อความที่คั่นด้วยแท็บ
copy["คัดลอก: สเปรดชีต"] --> cb["คลิปบอร์ด (หลายรูปแบบ)"]
cb --> rich["รูปแบบที่อุดมกว่า"]
cb --> plain["รูปแบบที่ธรรมดากว่า"]
rich --> f1["ส่วนตัวของแอป"]
rich --> f2["HTML Format"]
plain --> f3["CSV"]
plain --> f4["CF_UNICODETEXT"]
f2 -->|"Word"| word["ตารางที่จัดรูปแบบ"]
f4 -->|"Notepad"| notepad["ข้อความที่คั่นด้วยแท็บ"]
พูดกลับกัน ข้อร้องเรียนตอนเปิด — “การจัดรูปแบบพัง” “สิ่งแปลกถูกวาง” — เกือบทั้งหมดลดเหลือปัญหาของวิธีที่ฝั่งหนึ่งเลือกรูปแบบ หรือวิธีที่อีกฝั่งเสนอพวกมัน บทที่ 4 ครอบฝั่งวาง บทที่ 5 ครอบฝั่งคัดลอก
3. รูปแบบมาตรฐานกับรูปแบบที่ลงทะเบียน — CF_UNICODETEXT, CF_HDROP, HTML Format
3.1. รูปแบบมาตรฐาน — ใช้ฝั่งยูนิโค้ดสำหรับข้อความ
รูปแบบที่ OS กำหนดล่วงหน้าเรียกว่ารูปแบบมาตรฐาน อันที่ปรากฏตลอดในแอปธุรกิจมีดังต่อไปนี้3
| รูปแบบ | ค่า | เนื้อหา |
|---|---|---|
| CF_TEXT | 1 | ข้อความ ANSI (ขึ้นกับโค้ดเพจ) |
| CF_UNICODETEXT | 13 | ข้อความยูนิโค้ด นี่คือรูปแบบต้นแบบสำหรับข้อความ |
| CF_HDROP | 15 | รายการพาธไฟล์ (แฮนเดิล HDROP) |
| CF_DIB | 8 | บิตแมปที่ไม่ขึ้นกับอุปกรณ์ |
| CF_LOCALE | 16 | ตัวระบุโลแคลที่เกี่ยวกับข้อความ |
CF_TEXT และ CF_UNICODETEXT ถูกแปลงเข้าหากันโดยนัยโดยระบบ (รูปแบบสังเคราะห์) การแปลงรหัสอักขระใช้โค้ดเพจที่เกี่ยวกับ CF_LOCALE3 การพึ่งการแปลงนั้นทิ้งอักขระที่ ANSI แทนไม่ได้ (เช่น สัญลักษณ์เฉพาะยูนิโค้ดและอักขระรวม) ดังนั้นกฎคือรวมสิ่งที่แอปอ่านและเขียนบน CF_UNICODETEXT (DataFormats.UnicodeText ใน .NET)
flowchart TB
accTitle: การแปลงโดยนัยระหว่าง CF_UNICODETEXT กับ CF_TEXT
accDescr: แอปอ่านและเขียนเฉพาะ CF_UNICODETEXT ระบบสังเคราะห์ CF_TEXT ด้วยการแปลงโดยนัยด้วยโค้ดเพจของ CF_LOCALE อักขระที่ ANSI แทนไม่ได้ถูกทิ้งในการแปลงนั้น
apprw["แอปอ่านและเขียน"] --> uni["CF_UNICODETEXT"]
uni <-->|"การแปลง CF_LOCALE"| ansi["CF_TEXT (ANSI)"]
ansi -.-> loss["อักขระที่แทนไม่ได้ถูกทิ้ง"]
3.2. CF_HDROP — ไฟล์เดินทางเป็น “รายการพาธ”
CF_HDROP คือสิ่งที่ใช้เมื่อคุณคัดลอกไฟล์ใน File Explorer หรือเมื่อคุณลากและวางไฟล์ เพย์โหลดไม่ใช่ไฟล์เอง แต่เป็นบล็อกหน่วยความจำที่วางอาร์เรย์ที่ “จบด้วย NUL คู่”: หลังส่วนหัวโครงสร้าง DROPFILES สตริงพาธเต็มที่คั่นด้วยอักขระ NUL และสตริงว่างที่ท้าย pFiles ของส่วนหัวคือออฟเซ็ตเริ่มต้นของรายการพาธ และ fWide บอกว่าสตริงเป็นยูนิโค้ดหรือไม่4
[DROPFILES header: pFiles=start offset of the path list, fWide=1(Unicode)]
C:\data\a.txt(NUL)C:\data\b.txt(NUL)(NUL)
ในโค้ดเนทีฟคุณดึงทีละอันด้วย DragQueryFile ใน .NET คุณรับเป็น string[] ผ่าน DataFormats.FileDrop ความจริงที่ว่า “สิ่งที่เดินทางคือพาธอย่างเดียว ไม่ใช่ไฟล์เอง” จะสำคัญอีกครั้งใน D&D ของบทที่ 8 และ 9
flowchart TB
accTitle: เค้าโครงบล็อกหน่วยความจำของ CF_HDROP
accDescr: โครงสร้าง DROPFILES นั่งที่ต้นของหน่วยความจำโกลบอล pFiles คือออฟเซ็ตเริ่มต้นของรายการพาธ และ fWide บอกว่าเป็นยูนิโค้ดหรือไม่ พาธเต็มตามมา คั่นด้วย NUL และบล็อกจบด้วยสตริงว่าง (NUL คู่) สิ่งที่เดินทางคือพาธอย่างเดียว ไม่ใช่ไฟล์เอง
hdr["DROPFILES (pFiles / fWide)"] --> p1["C:\\data\\a.txt + NUL"]
p1 --> p2["C:\\data\\b.txt + NUL"]
p2 --> tail["สตริงว่าง (NUL คู่)"]
hdr -.-> note["พาธอย่างเดียวเดินทาง ไม่ใช่ไฟล์"]
3.3. รูปแบบที่ลงทะเบียน — RegisterClipboardFormat และ “HTML Format”
สำหรับข้อมูลที่รูปแบบมาตรฐานแสดงไม่ได้ แอปสามารถเลือกชื่อแล้วลงทะเบียนรูปแบบของตนเอง ส่งชื่อให้ RegisterClipboardFormat แล้วคุณได้รหัสรูปแบบกลับ การลงทะเบียนภายใต้ชื่อเดียวกันจากแอปอื่นคืนรหัสเดียวกัน ดังนั้นเมื่อคุณตกลงชื่อ คุณแบ่งปันข้อมูลระหว่างแอปได้1 เมื่อคุณส่งข้อมูลมีโครงสร้างในชุดแอปของตนเอง ใช้ชื่อที่จะไม่ชน เช่น KomuraSoft.Report.RowData
รูปแบบที่ลงทะเบียนตัวแทนคือ “HTML Format” สำหรับข้อความที่จัดรูปแบบ (คู่กับ RTF เป็นสองรูปแบบข้อความอุดมหลัก) เพย์โหลดคือข้อความ UTF-8 แต่มีโครงสร้างแปลก: ส่วนหัวที่แจกแจงออฟเซ็ตไบต์ ถูกแนบที่ด้านหน้า5
Version:0.9
StartHTML:<byte offset of the start of the whole HTML>
EndHTML:<byte offset of the end of the whole HTML>
StartFragment:<byte offset of the start of the fragment>
EndFragment:<byte offset of the end of the fragment>
<html><body>
<!--StartFragment--><b>bold</b> fragment text<!--EndFragment-->
</body></html>
แต่ละออฟเซ็ตคือตำแหน่งไบต์จากต้นของข้อมูล รวมส่วนหัวเอง แนวปฏิบัติทั่วไปคือจองความกว้างคงที่ (เช่น 10 หลัก) แล้วเขียนค่าที่วัดกลับหลังคุณสร้างตัวแล้ว StartFragment/EndFragment ทำเครื่องหมายต้นและท้ายของ “ชิ้นที่ผู้ใช้เลือกจริง” เป็นไบต์ (ไม่ใช่อักขระ) ใน UTF-8 ที่รวมภาษาญี่ปุ่น จำนวนอักขระกับจำนวนไบต์แยกจากกัน ดังนั้นหากคุณคำนวณออฟเซ็ตนี้ผิด การวางลงในแอปอื่นทิ้งต้นหรือท้าย หากคุณสร้าง HTML Format เอง คุณต้องเติมส่วนหัวด้วยตำแหน่งไบต์ที่วัดหลังเข้ารหัสเป็น UTF-85
flowchart TB
accTitle: ส่วนหัว HTML Format สัมพันธ์กับออฟเซ็ตอย่างไร
accDescr: StartHTML และ EndHTML ของส่วนหัวชี้ไปที่ HTML ทั้งก้อน และ StartFragment กับ EndFragment ชี้ไปที่ชิ้นที่ผู้ใช้เลือก ทั้งคู่เป็นตำแหน่งไบต์จากต้นของข้อมูล เพราะจำนวนอักขระกับจำนวนไบต์แยกใน UTF-8 ให้เติมส่วนหัวด้วยตำแหน่งไบต์ที่วัดหลังเข้ารหัส
header["ส่วนหัว (ออฟเซ็ตไบต์)"] --> html["HTML ทั้งก้อน"]
html --> frag["ชิ้นที่เลือก"]
header -.-> byte["ออฟเซ็ตคือไบต์หลัง UTF-8"]
CSV (DataFormats.CommaSeparatedValue ใน .NET) ก็ใช้กันทั่วไปสำหรับข้อมูลตาราง สำหรับการทำงานร่วมกับ Excel การเสนอ HTML Format (พร้อมการจัดรูปแบบ) CSV (ค่าอย่างเดียว) และ CF_UNICODETEXT (คั่นด้วยแท็บ) ด้วยกันหมายความว่าคุณไม่ต้องเลือกปลายทางการวางเดียว
4. แนวปฏิบัติฝั่งวาง — ลำดับความสำคัญของรูปแบบและการตรวจความถูกต้อง
4.1. มองจากรูปแบบที่อุดมลงมา
รูปแบบบนคลิปบอร์ดเรียงตามลำดับที่ฝั่งคัดลอกวางพวกมัน (นั่นคือจากที่แสดงได้มากกว่าไปยังที่น้อยกว่า) เส้นฐานของฝั่งวางคือมอง ในบรรดารูปแบบที่คุณจัดการได้ เริ่มจากอันที่มีข้อมูลมากที่สุด ใน Win32 คุณแจกแจงด้วย EnumClipboardFormats แล้วใช้รูปแบบแรกที่คุณรู้ หรือส่งรายการลำดับความสำคัญของตนเองให้ GetPriorityClipboardFormat แล้วให้มันเลือก2
ใน .NET กิ่งดูประมาณต่อไปนี้
// Pasting a table: look from rich to plain
var data = Clipboard.GetDataObject();
if (data is null) return;
// Advertising a format does not guarantee the payload is a string. Use this
// branch only when the type also checks out; otherwise fall through to the next candidate
if (data.GetDataPresent(DataFormats.Html)
&& data.GetData(DataFormats.Html) is string html)
{
// Validate the HTML Format header, then import as a table
}
else if (data.GetDataPresent(DataFormats.CommaSeparatedValue))
{
// Import as CSV
}
else if (data.GetDataPresent(DataFormats.UnicodeText))
{
// Import as tab-separated text
}
นั่นคือคำตอบต่อข้อร้องเรียนตอนเปิด “การวางตาราง Excel พัง” แอปที่อ่านเฉพาะข้อความธรรมดาไม่เคยได้รับโครงสร้างตาราง คุณรับลงไปในรายการรูปแบบไกลแค่ไหนคือการตัดสินออกแบบฝั่งวาง
flowchart TB
accTitle: กิ่งการวางที่มองจากรูปแบบที่อุดมลงมา
accDescr: หาก HTML Format มีอยู่และเพย์โหลดก็เป็นสตริง ให้นำเข้าเป็นตาราง มิฉะนั้นลอง CSV หากนั่นก็ขาด ให้ตกลงสู่ข้อความที่คั่นด้วยแท็บ หากไม่มีผู้สมัครใด ให้ปฏิเสธ
startsel["เริ่มวาง"] --> h{"HTML Format + สตริง?"}
h -->|"ใช่"| useh["ตรวจส่วนหัว → ตาราง"]
h -->|"ไม่"| c{"มี CSV?"}
c -->|"ใช่"| usec["นำเข้าเป็น CSV"]
c -->|"ไม่"| t{"UnicodeText?"}
t -->|"ใช่"| uset["ข้อความที่คั่นด้วยแท็บ"]
t -->|"ไม่"| giveup["ปฏิเสธ"]
4.2. ข้อมูลที่วางคืออินพุตภายนอก
พลาดง่าย แต่เนื้อหาคลิปบอร์ดคือข้อมูลจากภายนอก และคุณไม่รู้ว่าแอปใดวางพวกมัน Microsoft ยังเตือน ในเอกสารคลิปบอร์ด OLE ว่า “ข้อมูลคลิปบอร์ดไม่น่าเชื่อถือ จงแยกวิเคราะห์อย่างระมัดระวังก่อนใช้ในแอป”7
- ตรวจว่าออฟเซ็ตส่วนหัว HTML Format ไม่ชี้ไปนอกบัฟเฟอร์ (แอปที่ส่งส่วนหัวพังมีอยู่จริง)
- ค่าที่คุณนำเข้าเป็นตัวเลข วันที่ หรือรหัส ควรผ่านการตรวจความถูกต้องเดียวกันกับอินพุตบนหน้าจอ
- ใส่การป้องกันต่อข้อมูลยักษ์ แม้ใครวางภาพหลายร้อยเมกะไบต์หรือข้อความหลายล้านบรรทัด อย่าบล็อก UI และปฏิเสธเมื่อเกินขีด ข้อควรระวัง: GetData ของ .NET ชั่วขณะที่คุณเรียก ทำให้เพย์โหลดทั้งก้อนเป็นรูปเป็นร่างเป็นสตริงแมเนจด์ (และ delayed rendering รันเป็นส่วนหนึ่งของสิ่งนั้น) ดังนั้นการวางการตรวจขนาดหลัง GetData ไม่ใช่การป้องกัน ใน Win32 การตรวจ GlobalSize บน HGLOBAL ที่ GetClipboardData คืนให้การป้องกันที่ขั้นของ “อย่าเดินต่อไปสู่การแปลงและการแยกวิเคราะห์เป็นสตริงแมเนจด์” แต่สำหรับรูปแบบ delayed rendering GetClipboardData เองจุดการเรนเดอร์ ดังนั้นคุณยังกันการทำให้เป็นรูปเป็นร่างฝั่งต้นทางคัดลอกไม่ได้ เพื่อไม่ให้ UI แข็ง ให้ย้ายการดึงออกจากเธรด UI (และแม้เช่นนั้น เพราะ Clipboard ของ .NET ต้องการ STA ทำบนเธรดเฉพาะที่ตั้งเป็น STA ไม่ใช่บนเธรดพูลของ Task.Run (MTA) — ตอนที่ 5.1)
แนวคิดที่ว่า “ค่าที่มาจากภายนอก ไม่ว่าเส้นทางใด ถูกตรวจก่อนที่คุณใช้” คือแนวคิดเดียวกันที่วางใน “อย่าใช้ค่าที่ถอดรหัสจาก QR ตามที่เป็น” สมมติฐานที่ว่าการวางปลอดภัยเพราะเป็นการกระทำของผู้ใช้ คือวิธีที่อุบัติเหตุเริ่ม
flowchart TB
accTitle: ตรวจข้อมูลที่วางก่อนที่คุณใช้
accDescr: ข้อมูลที่เอาจากคลิปบอร์ดผ่านการมีรูปแบบ ชนิดเพย์โหลด ขีดขนาด และการตรวจเนื้อหาตามลำดับนั้น ล้มเหลวอย่างใดอย่างหนึ่งแล้วคุณปฏิเสธหรือตกลงสู่รูปแบบผู้สมัครถัดไป
present["มีรูปแบบ?"] --> type["ชนิดเพย์โหลดถูก?"]
type --> size["ขนาดอยู่ในขีด?"]
size --> content["ตรวจเนื้อหา"]
content --> ok["นำเข้า"]
type -.->|"ชนิดผิด"| rej["ปฏิเสธ / รูปแบบถัดไป"]
size -.->|"ใหญ่เกินไป"| rej
content -.->|"ไม่ถูกต้อง"| rej
5. แนวปฏิบัติฝั่งคัดลอก — การเสนอหลายรูปแบบพร้อมกัน และ delayed rendering
5.1. วางหลายรูปแบบพร้อมกัน
แนวปฏิบัติฝั่งคัดลอกคือกลับของ 4.1: เสนอรูปแบบที่อุดมและรูปแบบธรรมดาในเวลาเดียวกัน ด้วย DataObject ของ WinForms/WPF คุณเขียนได้ไม่กี่บรรทัด15
// WinForms (System.Windows.Forms). WPF is the same shape with System.Windows DataObject/Clipboard
var data = new DataObject();
data.SetData(DataFormats.Html, htmlFormatText); // HTML Format string including the header
data.SetData(DataFormats.CommaSeparatedValue, csv); // CSV
data.SetData(DataFormats.UnicodeText, plainText); // Plain text
Clipboard.SetDataObject(data, copy: true); // copy:true = keep after the app exits
สองหมายเหตุ ประการแรก คลาส Clipboard ของ .NET ใช้ได้เฉพาะจากเธรด STA15 เธรด UI ของ WinForms/WPF เป็น STA เพราะ [STAThread] ดังนั้นปกติไม่เป็นปัญหา แต่การแตะจากเธรดพื้นหลังล้มเหลว (พื้นฐาน STA/MTA อยู่ใน “พื้นฐาน COM STA/MTA”) ประการที่สอง ความหมายของ copy: true ผูกกับ delayed rendering ในตอนย่อยถัดไป
5.2. Delayed Rendering — ทำไม “ปิดต้นทางแล้ววางไม่ได้”
การสร้างเพย์โหลดใหญ่ในหลายรูปแบบทุกครั้งสิ้นเปลือง ดังนั้นคลิปบอร์ดมีกลไกชื่อ delayed rendering ส่ง NULL เป็นแฮนเดิลข้อมูลให้ SetClipboardData แล้ว แทนเพย์โหลด มีเพียงคำสัญญาว่าจะ “ผลิตเมื่อถูกขอ” ถูกลงทะเบียน เมื่อมีใครขอรูปแบบนั้น WM_RENDERFORMAT มาถึงต้นทางคัดลอก และเมื่อนั้นข้อมูลจึงถูกสร้าง2
ผลของการออกแบบนี้คือ “ฉันปิดแอปต้นทางแล้ววางไม่ได้อีก” ตอนเปิด ก่อนออก ต้นทางคัดลอกได้รับ WM_RENDERALLFORMATS และรับผิดชอบทำให้ทุกรูปแบบที่ยังไม่ถูกเรนเดอร์เป็นรูปเป็นร่าง ออกโดยไม่ทำสิ่งนั้นแล้วรูปแบบหายไป2
flowchart TB
accTitle: Delayed rendering และทำไมปิดแล้ววางจึงล้มเหลว
accDescr: ต้นทางคัดลอกลงทะเบียนเพียงคำสัญญาด้วยแฮนเดิล NULL และทำให้เป็นรูปเป็นร่างตามคำขอผ่าน WM_RENDERFORMAT ตอนออกมันรับผิดชอบทำให้ทุกรูปแบบเป็นรูปเป็นร่างด้วย WM_RENDERALLFORMATS ข้ามสิ่งนั้นแล้วรูปแบบหายไป
promise["SetClipboardData NULL = คำสัญญา"] --> req["ฝั่งวางขอ"]
req --> render["WM_RENDERFORMAT → สร้างตอนนี้"]
promise --> quit["ต้นทางคัดลอกกำลังจะออก"]
quit -->|"RENDERALLFORMATS"| ok["การวางทำงานหลังออก"]
quit -->|"ข้ามการทำให้เป็นรูปเป็นร่าง"| lost["รูปแบบหายหลังปิด"]
บนคลิปบอร์ด OLE (สไตล์ที่วาง IDataObject ด้วย OleSetClipboard) ความสัมพันธ์นี้ชัดยิ่งกว่า สิ่งที่คลิปบอร์ดถือคือพอยน์เตอร์ไปยังออบเจ็กต์ข้อมูล และการเรียก OleFlushClipboard ตอนแอปออกทำให้ข้อมูลเป็นรูปเป็นร่างบนคลิปบอร์ด ดังนั้นการวางยังทำงานหลังออก6 Clipboard.SetDataObject(data, copy: true) ของ .NET คือสิ่งที่ระบุพฤติกรรม “คงไว้หลังออก” นี้
เมื่อคุณคัดลอกช่วงใหญ่ใน Excel แล้วพยายามออก พร้อมต์ “มีข้อมูลจำนวนมากบนคลิปบอร์ด คุณอยากวางข้อมูลนี้ลงในโปรแกรมอื่นในภายหลังได้หรือไม่” คือการยืนยันพอดีว่าจะรันการทำให้เป็นรูปเป็นร่างนี้ (flush) หรือไม่ หากคุณใช้ delayed rendering ในแอปของตนเอง จำไว้ว่าการทำให้เป็นรูปเป็นร่างตอนออกเป็นส่วนหนึ่งของชุดเดียวกัน delayed rendering คือการเพิ่มประสิทธิภาพ และเพราะคำขอเรนเดอร์รันแบบซิงโครนัสภายในประมวลผลข้อความ ข้อมูลที่ใช้เวลานานในการสร้างมีการแลกของการทำให้ UI แข็ง2
6. แนวปฏิบัติสำหรับการเฝ้าคลิปบอร์ด — ผู้ฟัง การลองใหม่ และการยกเว้นประวัติ
6.1. ใช้ AddClipboardFormatListener
ข้อกำหนด เช่น “เราอยากตรวจจับค่าจากเครื่องอ่านบาร์โค้ดหรือสำเนาจากระบบธุรกิจแล้วนำเข้าอัตโนมัติ” ต้องการให้คุณเฝ้าการเปลี่ยนของคลิปบอร์ด ในประวัติมีสามวิธี วันนี้คำตอบที่ถูกมีหนึ่ง8
| วิธี | การประเมิน |
|---|---|
| อ่านบนตัวตั้งเวลา (โพล) | สิ้นเปลือง และคุณพลาดการอัปเดตได้ อย่าใช้ |
| SetClipboardViewer (โซ่ผู้ชม) | บั๊กในแอปหนึ่งในโซ่ทำลายทั้งโซ่ ถูกเก็บไว้เพื่อความเข้ากันได้ย้อนหลังเท่านั้น |
| AddClipboardFormatListener | แนะนำ WM_CLIPBOARDUPDATE มาถึงหน้าต่างที่ลงทะเบียน |
flowchart TB
accTitle: ลำดับของการเฝ้าคลิปบอร์ด
accDescr: ลงทะเบียนด้วย AddClipboardFormatListener เมื่อแฮนเดิลถูกสร้าง และ WM_CLIPBOARDUPDATE มาถึงไม่ว่าแอปใดคัดลอก อ่านพร้อมการลองใหม่ และยกเลิกการลงทะเบียนสมมาตรด้วย RemoveClipboardFormatListener เมื่อแฮนเดิลถูกทำลาย
created["AddClipboardFormatListener"] --> wait["รอ"]
anyapp["แอปใดคัดลอก"] --> notify["WM_CLIPBOARDUPDATE"]
wait --> notify
notify --> readtry["อ่านพร้อมการลองใหม่ (6.2)"]
readtry --> wait
destroyed["RemoveClipboardFormatListener"] -.->|"ยกเลิกการลงทะเบียน"| created
// Minimal WinForms implementation
public partial class MainForm : Form
{
[DllImport("user32.dll", SetLastError = true)]
static extern bool AddClipboardFormatListener(IntPtr hwnd);
[DllImport("user32.dll", SetLastError = true)]
static extern bool RemoveClipboardFormatListener(IntPtr hwnd);
const int WM_CLIPBOARDUPDATE = 0x031D;
protected override void OnHandleCreated(EventArgs e)
{
base.OnHandleCreated(e);
AddClipboardFormatListener(Handle);
}
protected override void OnHandleDestroyed(EventArgs e)
{
// Unregister symmetrically to match handle destruction / recreation
RemoveClipboardFormatListener(Handle);
base.OnHandleDestroyed(e);
}
protected override void WndProc(ref Message m)
{
if (m.Msg == WM_CLIPBOARDUPDATE)
{
// Read Clipboard.GetDataObject() here and import if the format is one you need
}
base.WndProc(ref m);
}
}
6.2. ลองใหม่เมื่อเปิดไม่ได้
มีเพียงหนึ่งหน้าต่างในแต่ละครั้ง ที่เปิดคลิปบอร์ดได้ ขณะที่โปรเซสอื่นเปิดอยู่ OpenClipboard ล้มเหลว2 ทันทีหลัง WM_CLIPBOARDUPDATE ต้นทางคัดลอกหรือผู้เฝ้าอื่นมักยังทำงานอยู่ ดังนั้นความล้มเหลวในการอ่านชั่วคราวคืออีเวนต์ปกติ ใส่การลองใหม่ไม่กี่ครั้งพร้อมรอสั้น (หลายสิบมิลลิวินาที) ระหว่างเสมอ โปรดทราบว่าโอเวอร์โหลด Clipboard ของ .NET ที่ให้คุณระบุจำนวนการลองใหม่และช่วงมีอยู่เฉพาะฝั่งเขียน SetDataObject ไม่มีเทียบเท่าฝั่งอ่าน (GetDataObject และพวก) ดังนั้นคุณเขียนจับ-รอ-ลองใหม่เอง — ExternalException บน WinForms, COMException บน WPF
flowchart TB
accTitle: ลำดับการลองใหม่อ่านคลิปบอร์ด
accDescr: มีเพียงหนึ่งหน้าต่างในแต่ละครั้งที่เปิดคลิปบอร์ดได้ ดังนั้นการอ่านทันทีหลังการแจ้งเปลี่ยนสามารถล้มเหลวด้วยการแข่งกับโปรเซสอื่น เมื่อมีข้อยกเว้น รอหลายสิบมิลลิวินาทีแล้วลองใหม่ หากถึงขีด ให้ยอมแพ้ครั้งนี้แล้วรับในการอัปเดตถัดไป
upd["WM_CLIPBOARDUPDATE"] --> tryread["พยายามอ่าน"]
tryread -->|"สำเร็จ"| useok["นำเข้า (การตรวจบทที่ 4)"]
tryread -->|"กำลังถูกใช้"| waitretry["รอหลายสิบมิลลิวินาที"]
waitretry -->|"ลองใหม่"| tryread
waitretry -->|"ขีด"| giveup2["ยอมแพ้ครั้งนี้"]
6.3. กันออกจากประวัติและการซิงค์ — ดูแลฟีเจอร์คัดลอกที่จัดการความลับ
Windows มีประวัติคลิปบอร์ด (Win+V) และการซิงค์ข้ามอุปกรณ์ (คลิปบอร์ดคลาวด์) และข้อมูลที่แอปวางอยู่ในขอบเขตของทั้งคู่โดยค่าเริ่มต้น แอปที่ใส่ความลับ เช่น รหัสผ่านหรือหมายเลขบัญชี บนฟีเจอร์คัดลอกยังวางรูปแบบที่ลงทะเบียนที่ยกเว้นเนื้อหาจากประวัติและการซิงค์1
- ExcludeClipboardContentFromMonitorProcessing: วางสิ่งนี้แล้วเนื้อหาของสำเนานั้นไม่ถูกรวมทั้งในประวัติและในการซิงค์
- CanIncludeInClipboardHistory (DWORD 0): ยับยั้งประวัติอย่างเดียว
- CanUploadToCloudClipboard (DWORD 0): ยับยั้งการซิงค์ข้ามอุปกรณ์อย่างเดียว
เหตุที่รหัสผ่านที่คัดลอกโดยตัวจัดการรหัสผ่านไม่เหลือบน Win+V คือกลไกนี้ คุณได้รหัสรูปแบบด้วยการส่งชื่อให้ RegisterClipboardFormat แล้วตั้งมันคู่กับข้อมูลธรรมดา ดังนั้นคุ้มที่จะอิมพลีเมนต์ในแอปธุรกิจใดที่จัดการความลับ
7. คลิปบอร์ดจากมุมไอที — การควบคุมประวัติ การซิงค์คลาวด์ และ RDP
ก้าวออกจากการพัฒนาเล็กน้อย นี่คือจุดที่สำคัญต่อผู้ดูแล ประวัติคลิปบอร์ดสะสมสำเนาล่าสุด และคลิปบอร์ดคลาวด์ซิงค์สำเนาข้ามอุปกรณ์ที่ลงชื่อเข้าด้วยบัญชี Microsoft / บัญชี Microsoft Entra เดียวกัน10 สะดวกเท่าที่เป็น มันยังผลิตของเหลือและการล้น: ข้อมูลส่วนบุคคลที่คัดลอกจากระบบธุรกิจสะสมในประวัติ และเนื้อหาที่คัดลอกบนพีซีงานซิงค์ไปพีซีส่วนตัว
นโยบายสองอย่างที่คุณใช้ควบคุมสิ่งนี้ในองค์กรมีดังต่อไปนี้
| สิ่งที่คุณควบคุม | GPO (Computer Configuration > Administrative Templates > System > OS Policies) | Policy CSP (Intune) | ค่าเริ่มต้น |
|---|---|---|---|
| ประวัติคลิปบอร์ด | Allow Clipboard History | Experience/AllowClipboardHistory | อนุญาต |
| การซิงค์ข้ามอุปกรณ์ | Allow Clipboard synchronization across devices | Privacy/AllowCrossDeviceClipboard | อนุญาต |
ทั้งคู่ใช้ได้จาก Windows 10 เวอร์ชัน 1809 เป็นต้นไป ปิดพวกมันแล้วรายการที่ตรงกันในแอปการตั้งค่าถูกทำให้เป็นสีเทา และนโยบายมีผลทันที910
ของประจำอีกอย่างคือการเปลี่ยนทิศทางคลิปบอร์ดของ RDP (Remote Desktop) โดยค่าเริ่มต้น คัดลอกและวางทำงานระหว่างพีซีท้องถิ่นกับเซสชันระยะไกล จึงกลายเป็นเส้นทางสำหรับเอาความลับออกจากเซิร์ฟเวอร์ได้ นโยบาย “Do not allow clipboard redirection” (ค่าเรจิสทรี fDisableClip) บล็อกได้ทั้งสองทิศทาง11 Windows Server / Windows 11 รุ่นล่าสุดยังเพิ่มนโยบายที่ละเอียดกว่า เช่น จำกัดทิศทางเซิร์ฟเวอร์ไปไคลเอนต์เป็นข้อความอย่างเดียว ว่าคุณจะห้ามเด็ดขาดหรือจำกัดเป็นขั้นคือสมดุลของการปฏิบัติการและความปลอดภัย
flowchart TB
accTitle: เส้นทางที่เนื้อหาคลิปบอร์ดกระจายได้ และจุดควบคุม
accDescr: เนื้อหาที่คัดลอกอยู่ในขอบเขตของประวัติและการซิงค์คลาวด์โดยค่าเริ่มต้น และบน RDP พวกมันเดินทางไปอีกเซสชันผ่านการเปลี่ยนทิศทาง แต่ละเส้นทางควบคุมด้วยนโยบายได้ และฝั่งแอปยกเว้นตนเองจากประวัติและการซิงค์ด้วยรูปแบบยกเว้นได้
cb["คลิปบอร์ด"] --> hist["ประวัติ (Win+V)"]
cb --> cloud["การซิงค์คลาวด์"]
cb --> rdp["การเปลี่ยนทิศทาง RDP"]
hist -.-> p1["AllowClipboardHistory"]
cloud -.-> p2["AllowCrossDeviceClipboard"]
rdp -.-> p3["fDisableClip"]
cb -.-> p4["รูปแบบยกเว้นของแอป (6.3)"]
8. ลากและวางคือ COM — IDataObject + IDropSource + IDropTarget
8.1. ข้อมูลเดียวกันกับคลิปบอร์ด วิธีถือที่ต่างกัน
ลากและวางแบบ OLE รันด้วยสามบทบาทต่อไปนี้12
| บทบาท | ใครอิมพลีเมนต์ | งาน |
|---|---|---|
| IDataObject | ต้นทางลาก | เพย์โหลดที่กำลังถือ ออบเจ็กต์ข้อมูลหลายรูปแบบเดียวกันกับคลิปบอร์ด |
| IDropSource | ต้นทางลาก | การตัดสินว่าการลากดำเนินต่อหรือถูกยกเลิก และการตอบกลับเคอร์เซอร์ |
| IDropTarget | เป้าหมายวาง | การประกาศรับ/ปฏิเสธใน DragEnter/DragOver/DragLeave/Drop และการรับการวาง |
ต้นทางลากเรียก DoDragDrop ลูปลากเริ่ม และเมื่อเมาส์เข้าหน้าต่างเป้าหมายวาง IDropTarget นั้นถูกแจ้ง ตอนวาง IDataObject ถูกส่งต่อ เอกสารทางการยังกล่าวว่า “D&D ให้ฟังก์ชันการทำงานเดียวกันพอดีกับคัดลอกและวางของคลิปบอร์ด หากแอปอิมพลีเมนต์คัดลอกและวางอยู่แล้ว การเพิ่มมีน้อย”12 กล่าวอีกนัย DataObject หลายรูปแบบที่คุณสร้างในบทที่ 2 ถึง 5 กลายเป็นเพย์โหลด D&D ตามที่เป็น
flowchart TB
accTitle: ลำดับของลากและวางแบบ OLE
accDescr: ต้นทางลากใส่ IDataObject ในเพย์โหลดแล้วเรียก DoDragDrop เพื่อเริ่มลูปลาก IDropTarget ของเป้าหมายวางประกาศรับ/ปฏิเสธใน DragEnter และ DragOver และตอน Drop เลือกรูปแบบจาก IDataObject แล้วดึงออก
src["IDataObject + IDropSource"] -->|"DoDragDrop"| loop["ลูปลาก"]
loop -->|"เมาส์เข้า"| enter["DragEnter/Over: Effect"]
enter -->|"ปุ่มขึ้น"| drop["IDropTarget.Drop"]
drop --> data["เลือกรูปแบบแล้วดึง"]
8.2. ต้องการ OleInitialize (STA)
หน้าต่างที่จะเป็นเป้าหมายวางลงทะเบียนด้วย RegisterDragDrop และมีกับดักคลาสสิกที่นี่ หากคุณเริ่มต้น COM ด้วย CoInitialize/CoInitializeEx RegisterDragDrop ล้มเหลวเสมอด้วย E_OUTOFMEMORY คุณต้องเริ่มต้นด้วย OleInitialize13 OleInitialize เริ่มต้น COM เป็น STA เพราะ D&D เป็นฟีเจอร์ที่รากอยู่ในโลก STA ของหน้าต่างและปั๊มข้อความ เธรดที่เรียกยังต้องรันปั๊มข้อความ ข้ามสิ่งนั้นแล้วแอปอื่นแฮงระหว่างการลาก13 พื้นหลังที่นี่คือการอภิปรายโมเดลเธรดพอดีใน “พื้นฐาน COM STA/MTA”
ในแอป WinForms/WPF เฟรมเวิร์กรับการเริ่มต้น OLE และการอิมพลีเมนต์อินเทอร์เฟซ ดังนั้นนักพัฒนาเพียงเขียนอีเวนต์
// WinForms: accept dropped files
listView1.AllowDrop = true;
listView1.DragEnter += (s, e) =>
{
// Also check that the source allows Copy (some sources only allow Move/Link)
e.Effect = e.Data.GetDataPresent(DataFormats.FileDrop)
&& (e.AllowedEffect & DragDropEffects.Copy) == DragDropEffects.Copy
? DragDropEffects.Copy // Accept: receive as a copy
: DragDropEffects.None; // Do not accept
};
listView1.DragDrop += (s, e) =>
{
// Drag data is also untrusted input. Even if it advertises FileDrop, the payload
// can be null or a different type, and GetData itself can fail
object data;
try { data = e.Data.GetData(DataFormats.FileDrop); }
catch (COMException) { return; }
if (data is not string[] paths) return;
foreach (var path in paths)
{
// Validate the path before importing (Section 9.3)
}
};
รูปเหมือนกันใน WPF: คุณรับด้วย AllowDrop="True" และอีเวนต์ DragOver/Drop บนอิลิเมนต์ และคุณดึงอาร์เรย์พาธด้วย e.Data.GetData(DataFormats.FileDrop) การประกาศรับ/ปฏิเสธ (Effect) บนทุก DragEnter/DragOver คือธรรมเนียมของ IDropTarget ข้ามสิ่งนั้นแล้วคุณได้บั๊กที่เคอร์เซอร์ค้างที่ “ไม่อนุญาต” และไม่เคยเปลี่ยน
9. กับดักของ D&D — การยกระดับ Move และการตรวจพาธ
9.1. คุณวางลงบนแอปที่ยกระดับเป็นผู้ดูแลระบบไม่ได้
วางไฟล์จาก File Explorer ลงบนแอปที่เปิดด้วย “รันในฐานะผู้ดูแลระบบ” แล้วไม่มีอะไรเกิด — นี่ไม่ใช่บั๊กการอิมพลีเมนต์ แต่เป็นพฤติกรรมของ OS UIPI (User Interface Privilege Isolation) บล็อกข้อความจากโปรเซสความสมบูรณ์ต่ำไปยังหน้าต่างความสมบูรณ์สูงโดยค่าเริ่มต้น ดังนั้นการแจ้งวางจาก File Explorer สิทธิ์ธรรมดา (ความสมบูรณ์กลาง) ไม่ถึงแอปที่ยกระดับ14
flowchart TB
accTitle: UIPI บล็อกการวางลงบนแอปที่ยกระดับอย่างไร
accDescr: การแจ้งวางจาก File Explorer ความสมบูรณ์กลางไปยังแอปที่ยกระดับความสมบูรณ์สูงถูกบล็อกโดย UIPI โดยค่าเริ่มต้นและไม่มาถึง คง UI ที่สิทธิ์ธรรมดาแล้วแยกงานที่มีสิทธิ์ และการวางมาถึง
explorer["Explorer (กลาง)"] -->|"แจ้งวาง"| uipi{"UIPI"}
uipi -->|"ถูกบล็อก"| elevated["แอปที่ยกระดับ: ไม่มีการวาง"]
uipi -->|"ผ่าน"| normal["UI ธรรมดา: การวางมาถึง"]
normal -.->|"มอบหมายงานที่มีสิทธิ์"| broker["โปรเซสที่ยกระดับที่แยก"]
ทางอ้อมที่อนุญาตข้อความเฉพาะ เช่น WM_DROPFILES เป็นรายตัวด้วย ChangeWindowMessageFilterEx เป็นที่รู้จักดี14 แต่สิ่งที่สิ่งนั้นปล่อยผ่านคือการแจ้งวางแบบเก่า (WM_DROPFILES) มันไม่แก้ OLE D&D ทั้งก้อน คำแนะนำเชิงปฏิบัติชัดเจน: หยุดออกแบบแอปให้รันยกระดับตลอดเวลา แยกเฉพาะงานที่ต้องการการยกระดับไปอีกโปรเซส และ UI เองอยู่ที่สิทธิ์ธรรมดาแล้วรับ D&D ได้ (การออกแบบการแยกมีรายละเอียดใน “วิธีแยก "เฉพาะงานที่ต้องใช้สิทธิ์ผู้ดูแลระบบ" ในแอป Windows อย่างเป็นรูปธรรม”)
9.2. DragDropEffects หมายความว่าอะไร — Move คือสัญญาว่า “ของเดิมหายไป”
Copy/Move/Link บน DragDropEffects ไม่ใช่การตกแต่ง พวกมันคือสัญญาระหว่างต้นทางลากกับเป้าหมายวาง ต้นทางลากประกาศชุดเอฟเฟกต์ที่อนุญาตใน DoDragDrop เป้าหมายวางเลือกเอฟเฟกต์จริง และเมื่อ Move สำเร็จ ต้นทางลากลบข้อมูล (ไฟล์) — นั่นคือธรรมเนียม หากฝั่งรับคืน Move โดยไม่คิด คุณได้อุบัติเหตุ “ฉันวางแล้วไฟล์เดิมหาย” สำหรับการนำเข้าของแอปธุรกิจ ฝั่งรับระบุ Copy คือค่าเริ่มต้นที่ปลอดภัย
flowchart TB
accTitle: สัญญา DragDropEffects — Move ลบของเดิม
accDescr: ต้นทางลากประกาศชุดเอฟเฟกต์ที่อนุญาตใน DoDragDrop และเป้าหมายวางเลือกเอฟเฟกต์จริง เมื่อ Move สำเร็จ ต้นทางลากลบไฟล์ ดังนั้นสำหรับการนำเข้า ฝั่งรับควรระบุ Copy
srcdecl["ต้นทาง: เอฟเฟกต์ที่อนุญาต"] --> tgtsel["เป้าหมาย: เลือก Effect"]
tgtsel -->|"Copy"| copyok["ของเดิมเหลือ (นำเข้า)"]
tgtsel -->|"Move"| moveact["ต้นทางลบไฟล์"]
9.3. การตรวจพาธที่วาง
สิ่งที่เดินทางใน CF_HDROP/FileDrop คือพาธอย่างเดียว (ตอนที่ 3.2) ก่อนนำเข้า ให้ผ่านการตรวจอินพุตที่ไม่น่าเชื่อถือเดียวกันกับการวาง
- ไฟล์หรือโฟลเดอร์: ตัดสินเป็นสเปกว่าเกิดอะไรเมื่อโฟลเดอร์ทั้งก้อนถูกวาง (ไล่ลงแล้วนำเข้า หรือปฏิเสธ)
- ตัวยึดตำแหน่ง OneDrive: พาธอาจมีอยู่ขณะตัวไฟล์ไม่ได้อยู่ท้องถิ่น — ไฟล์ตามคำขอ ชั่วขณะที่คุณเปิด การดาวน์โหลดเริ่ม และออฟไลน์มันล้มเหลว พฤติกรรมและมาตรการอยู่ใน “OneDrive "Files On-Demand" กับแอปธุรกิจ”
- พาธยาวและพาธแปลก: พาธเกิน MAX_PATH พาธเครือข่าย (UNC) และพาธบนสื่อถอดได้ควรถูกรับเฉพาะหลังคุณยืนยันว่าการประมวลผลปลายน้ำจัดการได้
- จำนวนและขนาดรวม: เพื่อไม่ให้การวางไฟล์หลายพันไฟล์ทำให้ UI แข็ง ให้นำเข้าแบบอะซิงโครนัสแล้วใส่ขีดกับการแสดงความคืบหน้า
10. สรุป
- คลิปบอร์ดคือกลไกที่วางเนื้อหาเดียวกันในหลายรูปแบบพร้อมกันในพื้นที่เดียวที่ใช้ร่วมภายในเดสก์ท็อปเดียวกัน (window station) ฝั่งวางเลือกรูปแบบ ดังนั้นสำเนาเดียวกันให้ผลต่างกัน
- ข้อความคือ CF_UNICODETEXT ไฟล์คือ CF_HDROP และข้อความที่จัดรูปแบบคือรูปแบบที่ลงทะเบียน HTML Format (ส่วนหัวออฟเซ็ตไบต์ + UTF-8)
- ฝั่งวางมองจากที่อุดมไปยังที่ธรรมดาและถือเพย์โหลดเป็นอินพุตภายนอก ฝั่งคัดลอกเสนอหลายรูปแบบพร้อมกัน และหากใช้ delayed rendering มันอิมพลีเมนต์การทำให้เป็นรูปเป็นร่างตอนออก (WM_RENDERALLFORMATS / OleFlushClipboard) ด้วย
- การเฝ้าคือ AddClipboardFormatListener + WM_CLIPBOARDUPDATE เตรียมสำหรับการแข่งของ OpenClipboard ด้วยการลองใหม่ และกันความลับออกจากประวัติและการซิงค์ด้วย ExcludeClipboardContentFromMonitorProcessing และพวก
- ไอทีควบคุมประวัติคลิปบอร์ด การซิงค์คลาวด์ และการเปลี่ยนทิศทาง RDP ได้ด้วย GPO / Intune ค่าเริ่มต้นคืออนุญาตทั้งหมด ดังนั้นตัดสินโดยเจตนาในสภาพแวดล้อมที่จัดการความลับ
- D&D คือ COM: IDropSource/IDropTarget ส่งผ่าน IDataObject เดียวกันกับคลิปบอร์ด RegisterDragDrop ต้องการ OleInitialize (STA)
- การวางลงบนแอปที่ยกระดับถูกบล็อกโดย UIPI Move บน DragDropEffects คือสัญญาว่า “ของเดิมหายไป” ตรวจพาธที่วางก่อนที่คุณนำเข้า
คัดลอกและวางกับ D&D สำหรับผู้ใช้คือฟีเจอร์ที่ควรรู้สึกเหมือนอากาศ นั่นพอดีคือเหตุที่ “ฉันวางไม่ได้” “มันพัง” และ “มันหาย” ทำร้ายประสบการณ์มากนัก — และเหตุที่แอปที่เสนอหลายรูปแบบและจัดการการวางอย่างถูกทำให้การปฏิบัติการประจำวันลื่นขึ้นด้วยตนเอง ฉันหวังว่านี่เป็นวัสดุที่มีประโยชน์เมื่อคุณตัดสินว่าจะแก้สิ่งใดก่อน
บทความที่เกี่ยวข้อง
- COM / ActiveX / OCX คืออะไร? - ความต่างและความสัมพันธ์ที่อธิบาย
- พื้นฐาน COM STA/MTA - โมเดลเธรดและวิธีหลีกเลี่ยงแฮง
- การรวม Windows Shell วันนี้ — เมนูบริบท การเชื่อมโยงไฟล์ และสิ่งที่เปลี่ยนใน Windows 11
- ทำไมโปรเซส EXCEL.EXE เหลือหลังการทำงานอัตโนมัติ Excel COM ของ C# — รูปแบบการปล่อยการอ้างอิงและการตัดสินแทนที่
- การออกแบบ UX ของแอป Windows - ลำดับความสำคัญตามสภาพแวดล้อมการใช้งาน
- OneDrive “Files On-Demand” กับแอปธุรกิจ — สมมติฐานที่ตัวยึดตำแหน่งทำลายและวิธีรับมือ
ด้านให้คำปรึกษาที่เกี่ยวข้อง
KomuraSoft LLC รับการออกแบบและการอิมพลีเมนต์ของการรองรับคัดลอกและวางกับลากและวางในแอปธุรกิจ (การเสนอหลายรูปแบบ การทำงานร่วมกับ Excel การนำเข้าไฟล์ที่วาง) การสอบสวนหาสาเหตุของปัญหา เช่น “มันพังเมื่อฉันวาง” หรือ “สำเนาหาย” ระบบอัตโนมัติของอินพุตที่เฝ้าคลิปบอร์ด และการอิมพลีเมนต์ที่กันข้อมูลลับออกจากประวัติและการซิงค์ กรณีที่เกี่ยวข้องกับชั้นล่างของ COM และ OLE ยินดีแม้คุณเริ่มจากการแยกอาการ
ลิงก์อ้างอิง
-
Microsoft Learn, Clipboard Formats. ว่าด้วยหน้าต่างสามารถวางข้อมูลเดียวกันในหลายรูปแบบคลิปบอร์ด รูปแบบที่ลงทะเบียนผ่าน RegisterClipboardFormat (การลงทะเบียนชื่อเดียวกันคืนค่าเดียวกัน จึงแอปแบ่งปันได้) รูปแบบสังเคราะห์ และการยกเว้นเนื้อหาจากประวัติคลิปบอร์ด / การซิงค์คลาวด์ด้วย ExcludeClipboardContentFromMonitorProcessing, CanIncludeInClipboardHistory และ CanUploadToCloudClipboard ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, Clipboard Operations. ว่าด้วยมีเพียงหนึ่งหน้าต่างในแต่ละครั้งที่เปิดคลิปบอร์ดได้ การวางรูปแบบจากที่แสดงได้มากกว่าไปยังที่น้อยกว่าตอนคัดลอก การเลือกรูปแบบตอนวางด้วย EnumClipboardFormats / GetPriorityClipboardFormat delayed rendering ด้วยการส่ง NULL ให้ SetClipboardData และความรับผิดชอบของ WM_RENDERFORMAT / WM_RENDERALLFORMATS และการแลกของ delayed rendering ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8
-
Microsoft Learn, Standard Clipboard Formats. ว่าด้วยนิยามของรูปแบบมาตรฐาน CF_TEXT (ANSI), CF_UNICODETEXT, CF_HDROP, CF_DIB และ CF_LOCALE และว่าระบบแปลง CF_TEXT กับ CF_UNICODETEXT โดยนัยโดยใช้โค้ดเพจที่เกี่ยวกับ CF_LOCALE ↩ ↩2 ↩3
-
Microsoft Learn, Shell Clipboard Formats. ว่าด้วย CF_HDROP ประกอบด้วยโครงสร้าง DROPFILES บวกอาร์เรย์ของสตริงพาธเต็มที่จบด้วย NUL คู่ การดึงพาธรายตัวด้วย DragQueryFile และรูปแบบเชลล์ CFSTR_ ต้องการการลงทะเบียนผ่าน RegisterClipboardFormat ↩ ↩2
-
Microsoft Learn, HTML Clipboard Format. ว่าด้วยชื่อที่ลงทะเบียนคือ “HTML Format” โครงสร้างส่วนหัวที่มีออฟเซ็ตไบต์ เช่น Version, StartHTML, EndHTML, StartFragment และ EndFragment การเข้ารหัสเป็น UTF-8 เสมอ และธรรมเนียมความเห็น StartFragment/EndFragment ↩ ↩2 ↩3
-
Microsoft Learn, OleFlushClipboard function (ole2.h). ว่าด้วย OleSetClipboard ทำให้คลิปบอร์ดถือเพียงพอยน์เตอร์ไปยังออบเจ็กต์ข้อมูล OleFlushClipboard ทำให้ข้อมูลเป็นรูปเป็นร่างบนคลิปบอร์ดจึงการวางยังทำงานหลังแอปออก และการเทคลิปบอร์ดด้วย OleSetClipboard(NULL) เมื่อคุณไม่ต้องคงไว้ตอนออก ↩ ↩2
-
Microsoft Learn, OleGetClipboard function (ole2.h). ว่าด้วยวิธีได้ IDataObject จากคลิปบอร์ด และคำเตือนว่าข้อมูลคลิปบอร์ดไม่น่าเชื่อถือและควรแยกวิเคราะห์อย่างระมัดระวังก่อนแอปใช้ ↩ ↩2
-
Microsoft Learn, Using the clipboard. ว่าด้วยการเปรียบเทียบสามวิธีเฝ้าคลิปบอร์ด (หน้าต่างผู้ชม หมายเลขลำดับ และผู้ฟังรูปแบบ) โปรแกรมใหม่ควรถูกคาดหวังให้ใช้ผู้ฟังผ่าน AddClipboardFormatListener โซ่ผู้ชมเปราะเมื่อการบำรุงโซ่ไม่สมบูรณ์ และหมายเลขลำดับไม่ใช่สิ่งที่คุณควรโพล ↩ ↩2
-
Microsoft Learn, Policy CSP - Experience. ว่าด้วยการอนุญาตหรือปฏิเสธประวัติคลิปบอร์ดด้วยนโยบาย Experience/AllowClipboardHistory ความพร้อมใช้จาก Windows 10 เวอร์ชัน 1809 เป็นต้นไป ค่าเริ่มต้นคืออนุญาต และการแมป GPO ภายใต้ “System > OS Policies” โดยการเปลี่ยนมีผลทันที ↩ ↩2
-
Microsoft Learn, Policy CSP - Privacy. ว่าด้วยการอนุญาตหรือปฏิเสธการซิงค์คลิปบอร์ดข้ามอุปกรณ์ด้วยนโยบาย Privacy/AllowCrossDeviceClipboard การซิงค์เกิดระหว่างอุปกรณ์ที่ลงชื่อเข้าด้วยบัญชี Microsoft / บัญชี Microsoft Entra เดียวกัน และค่าเริ่มต้นคืออนุญาต ↩ ↩2 ↩3
-
Microsoft Learn, Policy CSP - ADMX_TerminalServer. ว่าด้วย TS_CLIENT_CLIPBOARD (“Do not allow clipboard redirection” ค่าเรจิสทรี fDisableClip) สามารถห้ามการแชร์คลิปบอร์ดระหว่างท้องถิ่นกับระยะไกลในเซสชัน Remote Desktop และการเปลี่ยนทิศทางถูกอนุญาตโดยค่าเริ่มต้น ↩ ↩2
-
Microsoft Learn, Drag and Drop (COM). ว่าด้วยลากและวางแบบ OLE รันด้วยสามอย่างคือ IDropSource (ต้นทางลาก) IDropTarget (เป้าหมายวาง) และ DoDragDrop (ลูปที่ OLE จัดให้) การให้ฟังก์ชันการทำงานเดียวกันกับคัดลอกและวางของคลิปบอร์ด จึงแอปที่อิมพลีเมนต์คัดลอกและวางอยู่แล้วต้องการการเพิ่มน้อย และชนิดของการตอบกลับ ↩ ↩2 ↩3
-
Microsoft Learn, RegisterDragDrop function (ole2.h). ว่าด้วยการลงทะเบียนหน้าต่างเป้าหมายวางด้วย IDropTarget การล้มเหลวเสมอด้วย E_OUTOFMEMORY หาก COM ถูกเริ่มต้นด้วย CoInitialize/CoInitializeEx จึงต้องการ OleInitialize และการที่แอปต้นทางลากแฮงหากเธรดที่เรียกไม่รันปั๊มข้อความ ↩ ↩2 ↩3
-
Microsoft Learn, ChangeWindowMessageFilterEx function (winuser.h). ว่าด้วย UIPI เป็นกลไกความปลอดภัยที่โดยค่าเริ่มต้นบล็อกการรับข้อความจากผู้ส่งความสมบูรณ์ต่ำ และการอนุญาตข้อความเฉพาะรายหน้าต่างด้วยตัวกรองข้อความ (MSGFLT_ALLOW) ↩ ↩2 ↩3
-
Microsoft Learn, How to add data to the Clipboard (Windows Forms). ว่าด้วยการวางข้อมูลในหลายรูปแบบพร้อมกันด้วย DataObject และ Clipboard.SetDataObject การเพิ่มในหลายรูปแบบเพื่อให้แอปอื่นรู้จักได้ และคลาส Clipboard ใช้ได้เฉพาะจากเธรด STA จึงต้องการ [STAThread] ↩ ↩2
บทความที่เกี่ยวข้อง
บทความล่าสุดที่มีแท็กเดียวกัน เพื่อเจาะลึกหัวข้อใกล้เคียง
"ไม่ตอบสนอง" คืออะไรจริง ๆ — Windows ตัดสินว่าแอปแฮงอย่างไร และออกแบบอย่างไรให้ไม่แฮง
"ไม่ตอบสนอง" ของ Windows คือกลไกที่ OS ตัดสินว่าหน้าต่างไม่ได้ดึงข้อความเป็นเวลา 5 วินาทีแล้วแทนที่ด้วยหน้าต่างโกสต์ บทความนี้ครอบคลุมภาย...
Win32 Thread Pool API — คอนเคอร์เรนซีโดยไม่สร้างเธรดเอง ด้วย CreateThreadpoolWork
โค้ดเนทีฟของคุณกระจายการเรียก CreateThread ไปทั่วหรือไม่ บทความนี้อธิบาย Win32 thread pool API ที่ออกแบบใหม่ใน Vista — ออบเจ็กต์สี่ชนิด w...
Named Pipe ในทางปฏิบัติ — IPC มาตรฐานของ Windows ตั้งแต่การออกแบบถึงความปลอดภัย
คู่มือเชิงปฏิบัติเรื่อง named pipe ซึ่งเป็นการสื่อสารระหว่างโปรเซสมาตรฐานของ Windows บทความนี้จัดระเบียบจากแหล่งปฐมภูมิ ทั้งการเลือกระหว่...
แอปที่พังเมื่อรีซูมจากสลีป — อีเวนต์พลังงานของ Windows ทำงานอย่างไร และสร้างแอปธุรกิจที่รอดได้อย่างไร
เปิดแล็ปท็อปแล้วการเชื่อมต่อของแอปธุรกิจตาย — สาเหตุคือการออกแบบที่ไม่เคยคิดถึงสลีป บทความนี้ครอบคลุมลำดับการแจ้ง WM_POWERBROADCAST พฤติก...
DllMain และ Loader Lock — เหตุผลจริงที่ถูกบอกให้ "อย่าทำอะไรในการเริ่มต้น DLL"
ทำไมคุณต้องไม่เรียก LoadLibrary หรือซิงโครไนซ์กับเธรดอื่นจาก DllMain บทความนี้อธิบายจากแหล่งปฐมภูมิว่า loader lock ทำให้การแจ้ง DLL ทุกคร...
หัวข้อที่เกี่ยวข้อง
หน้าเหล่านี้วางหัวข้อไว้ในบริบทที่กว้างขึ้นของบริการและการตัดสินใจ
หัวข้อเทคนิคของ Windows
ประตูสู่การพัฒนา Windows การตรวจสอบบั๊ก และการใช้ประโยชน์จากสินทรัพย์เดิม
การย้าย ActiveX
การตัดสินใจว่าจะเก็บ ห่อหุ้ม หรือแทนที่คอมโพเนนต์ COM / ActiveX / OCX
เธรด UI และตัวจับเวลา
เธรด UI ของ WPF / WinForms, โฟลว์อะซิงโครนัส, Dispatcher และการออกแบบตัวจับเวลา
บริการที่เกี่ยวข้องกับหัวข้อนี้
บทความนี้เกี่ยวข้องโดยตรงกับบริการต่อไปนี้
พัฒนาแอป Windows
แอปธุรกิจ การเชื่อมต่ออุปกรณ์ และเครื่องมือสื่อสาร ตั้งแต่ความต้องการจนถึงการพัฒนา
ใช้ประโยชน์และย้ายสินทรัพย์เดิม
ใช้ประโยชน์และย้ายสินทรัพย์ COM / ActiveX / OCX และการพึ่งพา 32/64 บิต
คำถามที่พบบ่อย
คำถามที่มักพบในการปรึกษาเกี่ยวกับหัวข้อของบทความนี้
- ทำไมการจัดรูปแบบของตารางที่คัดลอกจาก Excel จึงพังเมื่อฉันวางลงในแอป
- คลิปบอร์ดไม่ได้ถือ "ข้อมูลชิ้นเดียว" เนื้อหาเดียวกันถูกวางในหลายรูปแบบพร้อมกัน (รูปแบบส่วนตัวของแอปต้นทาง HTML Format CSV ข้อความยูนิโค้ด เป็นต้น) และแอปปลายทางเลือกรูปแบบที่เข้าใจแล้วดึงสิ่งนั้น เมื่อการจัดรูปแบบพัง สาเหตุทั่วไปคือปลายทางอ่านเฉพาะข้อความธรรมดา (CF_UNICODETEXT) หากคุณอยากได้โครงสร้างตารางด้วย ให้อิมพลีเมนต์ฝั่งวางให้ชอบ HTML Format หรือ CSV ก่อน ในทางกลับกัน หากอยากให้แอปอื่นวางจากสำเนาที่ทำในแอปของคุณได้อย่างถูกต้อง ให้เสนอทั้งรูปแบบที่อุดมและรูปแบบธรรมดาตอนคัดลอก
- ทำไมฉันวางไม่ได้อีกหลังปิดแอปที่คัดลอกมา
- เพราะต้นทางใช้ delayed rendering แอปที่จัดการข้อมูลใหญ่ไม่วางเพย์โหลดตอนคัดลอก พวกมันลงทะเบียนเพียงคำสัญญาบนคลิปบอร์ดว่าจะ "ผลิตเมื่อถูกขอ" หากต้นทางจากนั้นออกโดยไม่ทำให้ข้อมูลเป็นรูปเป็นร่างเพื่อตอบ WM_RENDERALLFORMATS ตอนปิดระบบ รูปแบบใดที่ยังไม่ถูกเรนเดอร์จะหายไป แอปที่ใช้คลิปบอร์ด OLE (IDataObject) คงการวางให้ทำงานหลังออกได้ด้วยการเรียก OleFlushClipboard ตอนปิดระบบเพื่อทำให้ข้อมูลเป็นรูปเป็นร่าง
- แอปของฉันเฝ้าการเปลี่ยนของคลิปบอร์ดได้อย่างไร
- วิธีที่แนะนำในปัจจุบันคือลงทะเบียนหน้าต่างของคุณเป็นผู้ฟังด้วย AddClipboardFormatListener แล้วจัดการข้อความ WM_CLIPBOARDUPDATE ที่มาถึงทุกครั้งที่เนื้อหาเปลี่ยน การโพลเนื้อหาบนตัวตั้งเวลาสิ้นเปลืองงานและพลาดการอัปเดตได้ และโซ่ผู้ชมเก่าที่อิง SetClipboardViewer ถูกเก็บไว้เพื่อความเข้ากันได้ย้อนหลังเท่านั้น เพราะบั๊กในแอปหนึ่งในโซ่ทำลายทั้งโซ่ โปรดทราบด้วยว่า OpenClipboard ตอนอ่านสามารถล้มเหลวเพราะโปรเซสอื่นถือคลิปบอร์ด ดังนั้นอิมพลีเมนต์การลองใหม่พร้อมรอสั้นหากอยากให้อ่านเสถียร
- มีวิธีกันความลับ เช่น รหัสผ่าน ออกจากประวัติคลิปบอร์ด (Win+V) หรือไม่
- มีสองคันโยก หนึ่งฝั่งแอปและหนึ่งฝั่งนโยบาย ฝั่งแอป หากคุณยังวางรูปแบบที่ลงทะเบียน ExcludeClipboardContentFromMonitorProcessing เมื่อคัดลอก เนื้อหานั้นไม่ถูกรวมทั้งในประวัติและในการซิงค์ข้ามอุปกรณ์ คุณยังควบคุมแต่ละอย่างอิสระได้ด้วย CanIncludeInClipboardHistory (ประวัติอย่างเดียว) และ CanUploadToCloudClipboard (ซิงค์อย่างเดียว) นี่คือกลไกที่ตัวจัดการรหัสผ่านใช้ หากอยากปิดทั้งองค์กร คุณปิดประวัติและการซิงค์คลาวด์เองได้ด้วย AllowClipboardHistory และ AllowCrossDeviceClipboard ผ่าน Group Policy หรือ Intune (Policy CSP)
- ทำไมฉันลากและวางไฟล์ลงบนแอปที่รันในฐานะผู้ดูแลระบบไม่ได้
- เพราะกลไกความปลอดภัยชื่อ UIPI (User Interface Privilege Isolation) บล็อกการส่งข้อความจากโปรเซสความสมบูรณ์ต่ำไปยังหน้าต่างความสมบูรณ์สูง File Explorer รันที่สิทธิ์ธรรมดา (ความสมบูรณ์กลาง) ดังนั้นการแจ้งลากและวางไม่ถึงหน้าต่างของแอปที่ยกระดับ ทางอ้อมที่อนุญาตข้อความ เช่น WM_DROPFILES เป็นรายตัวด้วย ChangeWindowMessageFilterEx เป็นที่รู้จักดี แต่ใช้ได้เฉพาะการแจ้งวางแบบเก่า การแก้จริงคือหยุดออกแบบแอปให้รันยกระดับตลอดเวลา และแยกเฉพาะงานที่ต้องการการยกระดับไปอีกโปรเซส