[Tapuz .Net] Winfrosm DateTimerPicker ValueChanged and Windows Messages
שאלה:
אני רוצה ליצור מתודה שתפעל כל פעם שנבחר תאריך ב-DateTimePicker.
הכוונה היא לא כל פעם שנבחר תאריך שונה (ValueChanged) אלא יכול להיבחר לדוגמה גם אותו תאריך שהיה קודם.
יש אפשרות לעשות כך? אולי בשיטה עוקפת?
תשובה:
זאת שאלה מצויינת שבאמת תאפשר לנו להיכנס לעומק הקורה עם Windows Forms.
בואו נראה קודם מה השלבים שנעבור עד פתרון הבעיה.
נתחיל בלהדגים את הבעיה.
נפתח טופס Winform חדש.
נגרור לטופס פקד DateTimerPicker.
כמו כן, נוסיף לטופס TextBox שאליו נדפיס טקסט.
נירשם לאירוע ה-ValueChanged של ה-DateTimePicker שלנו ע"י לחיצה כפולה על ValueChanged.
נוסיף בהרשמה לאירוע קוד שידפיס את התאריך הנבחר לתוך ה-TextBox שלנו.
private void dateTimePicker1_ValueChanged(object sender, EventArgs e)
{
tbxConsole.Text += dateTimePicker1.Value.ToShortDateString() + Environment.NewLine;
}
בואו נריץ את האפליקציה.
נבחר את תאריך ה-25.2.2008.
נכתוב בתוך ה-DateTimerPicker שלנו את התאריך 27.2.008.
עכשיו ננסה לבחור שוב את ה-27.2.2008.
שימו לב אחרי שבחרנו את ה-27.2.2008 אחרי שהוא כבר היה התאריך הנבחר - שום דבר לא השתנה.
שזה אומר - ValueChanged יקפוץ אך ורק כאשר Value באמת משתנה.
השאלה שאנחנו מנסים לענות - איך לתפוס כל קביעה של תאריך ולא רק שינוי תאריך.
נפתח את Reflector על DateTimePicker במתודת ה-OnValueChanged שמריצה את ValueChanged.
(Reflector --> System.Windows.Forms --> System.WindowsForms.dll --> System.Windows.Forms --> DateTimePicker)
נבחר שאנחנו רוצים לראות מי קורא למתודת ה-OnValueChanged.
אפשר לראות שיש 3 פונקציות בתוך הפריימוורק שמרימות את האירוע.
הראשונה היא ResetValue והגיוני שהיא תרים אירוע שינוי ל-Value.
השנייה היא set_value שמתייחסת ל-Setter של המאפיין (גם באנגלית: Property) בשם Value. גם פה הגיוני שהאירוע ירוץ.
והשלישי הוא WmDateTimerChange שמקבל איזה פרמטר מוזר מסוג &Message.
את שני הראשונים הבנו והסכמנו שזה הגיוני רק לפי שם המתודה ששם צריך לעלות את האירוע, אבל מה זאת המתודה השלישית הזאת?
נבדוק מה הקוד שיש בתוך המתודה הזאת בפריימוורק.
אומנם זה קוד קצת מפחיד משהו, אבל אפשר לראות שהתנאי if האחרון שם בודק אם התאריך השתנה ורק אם כן מעלה את האירוע ValueChanged.
כלומר, כאן נמצא האשם שלנו בזה שלחיצה על תאריך שכבר נבחר לא מעלה ValueChanged.
עכשיו שמצאנו את האשם הראשי - נמצא את השותפים לפשע.
במתודה הזאת אני לא רואה שום נקודת התערבות טובה.
נראה מי קורה למתודה הזאת (דרך Analyze כפי שעשינו קודם לכן).
אפשר לראות כאן שרשרת של פונקציות של WndProc שקורה ל-WmReflectCommand שקורה ל-WmDateTimeChanged.
אני עדיין בכלל לא יודע מה זה WndProc ומה זה &Message ובכלל מה זה Wm.
נראה מה התוכן של שתי המתודות האלו.
אם נעקוב אחרי התהליך נראה ש-WndProc מקבל איזה פרמטר מוזר ואם הוא מקבל Message.Msg == 0x204e הוא יקרה ל-WmReflectCommand.
ב-WmReflectCommand אם נקבל IParamCode.code == -759 אז נקרא ל-WmDateTimeChange שבתורה אנחנו יודעים שמרים את אירוע ה-ValueChanged.
זאת השרשרת שגילינו.
הבעיה היא בשלב האחרון בין העברה במתודות בתוך הפרימוורק, יש שם איזה תנאי שמפריע לנו ולא מתאים.
זהו, זהינו לחלוטין את האשמים בבעיה שיש לנו.
עכשיו פתרון - הנקודת התערבות היחידה שיש לנו היא לדרוס את מתודת ה-WndProc בראש הערימה.
נפתח פרוייקט חדש מסוג Windows Control Library ב-Visual Studio 2008.
ניצור מחלקה (גם באנגלית: Class) חדשה בשם myDateTimerPicker בתוך הפרוייקט Windows Forms Control Library החדש שלנו.
נדאג לשנות את הקובץ ברירת מחדל כך שהמחלקה תירש (גם באנגלית: Inherit) את DateTimerPicker.
using System.Windows.Forms;
namespace WinControls_VS2008Testing
{
public class myDateTimerPicker : DateTimePicker
{
}
}
בתור התחלה לפתרון, נרצה לחשוף אירוע חדש בשם ValueChanging שירוץ לפני ValueChanged.
public class myDateTimerPicker : DateTimePicker
{
public event EventHandler ValueChanging;
public void OnValueChanging(EventArgs e)
{
if (ValueChanging != null)
{
ValueChanging(this, e);
}
}
}
הקוד יחסית בסיסי, הוספנו אירוע בשם ValueChanged ומתודת OnValueChanging שתעלה את האירוע.
בשלב זה נרצה להתערב הכי נמוך שאפשר בהיררכיה שמצאנו, שזו מתודת ה-WndProc.
public class myDateTimerPicker : DateTimePicker
{
protected override void WndProc(ref Message m)
{
base.WndProc(ref m);
}
public event EventHandler ValueChanging;
public void OnValueChanging(EventArgs e)
{
if (ValueChanging != null)
{
ValueChanging(this, e);
}
}
}
אז דרסנו את WndProc.
כרגע הוא רץ בדיוק אותו דבר כמו קודם ועוד אין שינוי.
נממש את שני התנאים מקודם לכן (שני ה-Ifים בתוך WndProc ו-WmReflectCommand).
using System;
using System.Runtime.InteropServices;
using System.Windows.Forms;
namespace WinControls_VS2008Testing
{
public class myDateTimerPicker : DateTimePicker
{
protected override void WndProc(ref Message m)
{
if ((m.Msg == 0x204e)
&& (m.HWnd == base.Handle)
&& (((NMHDR)m.GetLParam(typeof(NMHDR))).code == -759)
)
{
}
base.WndProc(ref m);
}
[StructLayout(LayoutKind.Sequential)]
private struct NMHDR
{
public IntPtr hwndFrom;
public IntPtr idFrom;
public int code;
}
public event EventHandler ValueChanging;
public void OnValueChanging(EventArgs e)
{
if (ValueChanging != null)
{
ValueChanging(this, e);
}
}
}
}
יש כאן קצת משחקים עם מחלקות ופרמטרים, אבל סה"כ העתקתי את התנאי מתוך Reflector ולא עשיתי איזה דבר יוצא דופן.
וכשלב אחרון בפתרון, נעלה בתנאים אלו את אירוע ה-OnValueChanging שלנו.
public class myDateTimerPicker : DateTimePicker
{
protected override void WndProc(ref Message m)
{
if ((m.Msg == 0x204e)
&& (m.HWnd == base.Handle)
&& (((NMHDR)m.GetLParam(typeof(NMHDR))).code == -759)
)
{
OnValueChanging(new EventArgs());
}
base.WndProc(ref m);
}
[StructLayout(LayoutKind.Sequential)]
private struct NMHDR
{
public IntPtr hwndFrom;
public IntPtr idFrom;
public int code;
}
public event EventHandler ValueChanging;
public void OnValueChanging(EventArgs e)
{
if (ValueChanging != null)
{
ValueChanging(this, e);
}
}
}
בואו נראה שהפתרון שלנו באמת פועל.
צריך לדאוג להוסיף Reference בין שני הפרוייקטים.
אחרי שנקמפל את כל ה-Solution נוכל לראות ב-Toolbox של הטופס שלנו את myDateTimePicker מתחת ל-Windows Forms Control Library שלנו.
נשים myDateTimePicker על הטופס.
(שלנו זה התחתון)
נירשם לאירוע ה-ValueChanging ושם נדאג להדפיס ל-TextBox.
private void myDateTimerPicker1_ValueChanging(object sender, EventArgs e)
{
tbxConsole.Text += myDateTimerPicker1.Value.ToShortDateString() + Environment.NewLine;
}
נריץ את האפליקציה.
נבחר מחדש את התאריך של היום.
באמת ניתן לראות שהאירוע עולה והערך מודפס כעת למרות שזה היה התאריך שנבחר קודם לכן.
עכשיו בואו נבין מה בכלל הולך כאן.
מה זה ה-Message הזה שעובר הלוך ושוב.
Windows Forms בכלל הוא ברובו שכבה של קוד דוט-נטי מעל אלמנטים חלונאיים סטנדרטיים.
כלומר, במערכת ההפעלה קיים איזה Unmanaged DateTimerPicker שכל אפליקציה יכולה לגשת אליו, לאתחל אותו ולהירשם לקבל Messages.
Winforms היא שכבת ביינים בין הסיוט שהוא תכנות Win32 לבין קוד-נטי.
למעשה Winforms יוצר כדי לטווח בין תכנות Object-Oriented שהוא הרבה יותר אינטואטיבי לבין תכנות Win32 שהוא הרבה יותר פרוצדוראלי.
מדי פעם לפעם, צריך לדעת איזה הודעות עוברת בין Winforms ו-Win32 כדי להרחיב פקדים קיימים, כמו במקרה הזה.
נסכם מה שעשינו.
ניתן להוריד את הקוד שעבדנו עליו מ- http://www.JustinAngel.Net/files/Example-DateTimerPicker-ValueChanging.zip.
קישור: http://www.tapuz.co.il/tapuzforum/main/Viewmsg.asp?forum=831&msgid=112949617