ภาษา: Visual C++
Application Type: Windows Forms Application
ก่อนหน้านี้ ผมมีความต้องการที่จะบันทึกค่า Configuration ของโปรแกรมไว้ เพื่อให้ผู้ใช้สามารถปรับแต่งโปรแกรมให้ทำงานตามความเหมาะสมของเขาได้ เช่น โปรแกรมตรวจนับจำนวนเซลล์มะเร็ง (Cell Image Analyzer, CIA) ที่ผมกำลังพัฒนาอยู่นั้น การแสดงผลเส้นรอบรูปเซลล์ที่ตรวจพบจะใช้สีอะไร ขนาดของเส้นเท่าไหร่ จะแสดงเป็นเส้นทึบหรือเส้นประ ฯลฯ ความจริงเราก็สามารถจะ Hard code ไว้ในโปรแกรมเลยก็ได้ แต่เนื่องจากโปรแกรมนี้ ผมจะพยายามพัฒนาให้มันสามารถนำไปประยุกต์ใช้กับการนับเซลล์ชนิดอื่นๆได้ด้วย จึงคิดว่า ผู้ใช้เองก็อาจจะต้องการให้แสดงสีที่แตกต่างกันออกไป เพื่อให้อ่านผลที่รายงานได้ชัดเจนยิ่งขึ้น ผมจึงจำเป็นต้องไม่ Hard code ไว้ในโปรแกรม
อ่าน/เขียน Registry
ในขั้นตอนการอ่าน/เขียนค่า Configuration นั้น ผมเลือกใช้การอ่าน/เขียนค่าไว้ใน Registry ของผู้ใช้ โดยเราจะสามารถอ้างถึง Registry ของโปรแกรมในพื้นที่ของผู้ใช้นั้นๆได้ที่ HKEY_CURRENT_USER\Software\(Manufacturer)\(Product)\(Version) และสามารถตรวจสอบดูได้ด้วยโปรแกรม regedit.exe นะครับ
สำหรับคำสั่งของ .NET ที่เขียนเป็นแบบ Windows Forms Application นั้น เราสามารถใช้คลาส System.Windows.Forms.Application ในการทำอะไรที่เกี่ยวกับโปรแกรมของเราได้หลายอย่าง รวมถึงการใช้ Property ที่ชื่อว่า UserAppDataRegistry ในการเข้าถึง Registry ของโปรแกรมในพื้นที่ของผู้ใช้นั้นๆ เช่น
Microsoft::Win32::RegistryKey^ urk = System::Windows::Forms::Application::UserAppDataRegistry;
String^ str = (String^)urk->GetValue("MyStringValue");
ซึ่งคำสั่ง GetValue() นี้ จะได้ค่ามาเป็น Object โดยที่ข้อมูลใน Registry นั้น เราสามารถเก็บค่าได้ 6 ชนิด คือ
- String (REG_SZ) - เก็บข้อความที่จบด้วย null character
- Binary (REG_BINARY) - ใช้เก็บข้อมูลใดๆในแบบ binary
- DWord (REG_DWORD) - เก็บค่า 32-bits
- QWord (REG_QWORD) - เก็บค่า 64-bits
- MultiString (REG_MULTI_SZ) - เก็บค่าเป็น array ของข้อความที่จบด้วย null character โดยจะจบข้อความทั้งหมดด้วย null character 2 ตัว
- ExpandString (REG_EXPAND_SZ) - เก็บข้อความที่จบด้วย null ที่มีค่าตัวแปรของระบบอยู่ด้วย เช่น %PATH% ข้อความที่อ่านมาได้นั้น จะมีการแทนค่าตัวแปรให้เรียบร้อยด้วยค่าจากตัวแปรในขณะนั้น
และเนื่องจากค่าที่อ่านมาได้จาก GetValue() นั้นเป็น Object เมื่อเราต้องการนำไปใช้ เราจะต้อง casting Object ให้ถูกต้องตรงกับชนิดของมัน ซึ่งเรารู้แน่นอนอยู่แล้วว่าเราเก็บค่าชนิดใดไว้ หรือเราอาจจะตรวจสอบชนิดของข้อมูลที่เก็บไว้ ว่าเป็นชนิดอะไรได้ด้วยคำสั่ง GetValueKind()
ปัญหาก็มีอยู่ว่า ถ้าเราต้องการที่จะเก็บค่าอื่นๆนอกเหนือจาก 6 แบบนี้ จะทำได้อย่างไร
เช่น ผมมีตัวแปรชนิด Point เก็บตำแหน่งของหน้าต่าง ซึ่งมี 2 ค่า คือ x และ y เป็นชนิด int ถ้าเราจะแยกเก็บ ก็สามารถทำได้แบบง่ายๆคือ แยกเก็บ PointX ตัวหนึ่ง PointY ตัวหนึ่ง เวลาอ่านค่ามา ก็อ่านมา 2 ครั้ง คือ GetValue("PointX") และ GetValue("PointY") จากนั้นก็เอาค่า x และ y ที่ได้มาประกอบกลับมาเป็น Point ใหม่ ซึ่งเขียนได้เลยทันที ไม่ต้องไปคิดอะไรมาก แต่เราต้องมาจัดการกับค่าตัวแปรที่ซับซ้อนเหล่านี้ทีละตัว ทั้งตอนอ่าน และตอนเขียน ซึ่งถ้าหากเรามีค่าตัวแปรหลายๆตัวที่ต้องการจะเก็บ และแต่ละตัวก็มีชนิดที่ซับซ้อนแตกต่างกันไป ก็อาจจะเกิดความยุ่งยากขึ้นได้ และอีกเหตุผลหนึ่งก็คือ ผมเองก็ขี้เกียจเขียนอะไรยาวๆ โดยเฉพาะอย่างยิ่ง ถ้าเราจะต้องกลับมาแก้
กลไก ToString()
เมื่อเราสั่ง SetValue() โดยส่ง Object ไปให้ และไม่ได้ระบุว่าให้จัดเก็บแบบไหน สำหรับ Object ที่มีชนิดไม่ตรงกับ 6 ชนิดที่ระบุไว้ คำสั่งนี้จะแปลงค่าไปเป็นข้อความ และเก็บไว้เป็นข้อความด้วยกลไกของคำสั่ง ToString() เช่น ถ้าเราสั่ง SetValue("TestPoint", gcnew Point(12,13)) ค่าที่เก็บไว้ใน Registry จะกลายเป็นข้อความ "{X=12,Y=13}" และเมื่ออ่านมาก็จะต้องอ่านมาเป็น String แล้วเราจะต้องจัดการแปลงข้อความนั้นให้กลับมาเป็น Point
กลไก ToString() นี้ เป็นกลไกที่ใช้ได้กับทุกๆ Object เพราะเป็นคำสั่งพื้นฐานของ Object เลย โดยปกติแล้ว ทั้งภาษา Java และ C# ก็มีกลไกนี้เพื่อใช้ในการแสดงตัวของ instance หรือที่เรียกว่า Reflection ทำให้สะดวกในการทำงานกับ Object ที่หลากหลาย โดยเฉพาะอย่างยิ่งการ debug ซึ่งถ้าเราต้องการให้ Object ใดๆ แสดงตัวออกมาเป็นข้อความอย่างไร เราก็ต้อง override คำสั่งนี้ อย่างเช่น Point นั้น ก็มีการ override คำสั่งนี้ กลายเป็นการให้ข้อความ "{X=?,Y=?}" ขึ้นอยู่กับค่า x และ y ในขณะนั้น
ทีนี้ปัญหาก็จะเหลือแค่ตอนที่เราจะแปลงค่ากลับจาก String มาเป็น Object ซึ่งตรงนี้ .NET ได้มีคลาส System.ComponentModel.TypeConverter ให้เราใช้ โดยที่เราจะสามารถร้องขอ TypeConverter สำหรับ Object ชนิดที่เราต้องการ เพื่อนำมาแปลงค่าได้ ถ้ามี TypeConverter ที่ตรงกันอยู่ เราก็สามารถนำมาใช้ได้เลย เช่น
Point^ t;
TypeConverter^ converter = TypeDescriptor::GetConverter(Point::typeid);
String^ strvalue = (String^)urk->GetValue("TestPoint");
if ((strvalue != nullptr) && (converter != nullptr))
t = converter->ConvertFromString(strvalue);
กลไก TypeConverter
กลไก TypeConverter นี้มีประโยชน์อย่างมาก และใช้งานอยู่เบื้องหลังในหลายๆเรื่อง เช่น การที่ Visual Studio สามารถแสดงหน้าต่างโปรแกรมตอนที่ออกแบบ และแสดงค่าต่างๆในหน้าต่าง Property และให้เรา set ค่าต่างๆของ Component ที่เรากำลังออกแบบอยู่ได้อย่าง real-time ได้นั้น ก็ใช้กลไกอันนี้ ดังนั้น สำหรับ TypeConverter ของ Object แต่ละชนิดที่เป็นคลาสสำคัญๆของ .NET ได้มีการเขียนเอาไว้ให้เราใช้หมดแล้ว เช่น ArrayConverter, BaseNumberConverter, BooleanConverter, ByteConverter, CharConverter, ฯลฯ (สามารถดูรายชื่อได้จาก MSDN Library ใน System.ComponentModel namespace)
แต่สำหรับคลาสชนิดใหม่ๆที่เราสร้างขึ้นมาเองนั้น เราจะต้องสร้างตัว TypeConverter สำหรับคลาสของเราขึ้นมาเอง โดยจะต้องสืบทอดมาจาก TypeConverter เช่น สมมติว่า เราต้องการสร้างคลาสชื่อว่า MyPoint3D โดยมีค่า x,y และ z สำหรับเก็บตำแหน่งในสามมิติ โดยเราจะต้อง override ToString() ด้วยดังนี้
public ref class MyPoint3D
{
public:
int x,y,z;
MyPoint3D(int x, int y, int z)
{
this->x = x;
this->y = y;
this->z = z;
}
virtual String^ ToString() override
{
return String::Format("[{0},{1},{2}]", x, y, z); // มีรูปแบบคือ "[x,y,z]"
}
};
จากนั้นสร้าง MyPoint3DConverter โดยสืบทอดมาจาก TypeConverter และ override คำสั่ง CanConvertFrom() CanConvertTo() ConvertFrom() แล ConvertTo() ดังนี้
public ref class MyPoint3DConverter : public System::ComponentModel::TypeConverter
{
public:
virtual bool CanConvertFrom(ITypeDescriptorContext^ context, Type^ sourceType) override
{
if (sourceType == String::typeid) {
return true; // ถ้าเป็น String ก็สามารถ convert ได้
}
// ไม่อย่างนั้นก็ให้เป็นหน้าที่ของคลาสที่เราสืบทอดมา
return TypeConverter::CanConvertFrom(context, sourceType);
}
// Overrides the ConvertFrom method of TypeConverter.
virtual Object^ ConvertFrom(ITypeDescriptorContext^ context,
CultureInfo^ culture, Object^ value) override
{
if (value->GetType() == String::typeid) {
String^ str = (String^)value;
array<Char>^ chars = {'[', ',', ']'};
array<String^>^ v = str->Split(chars);
return gcnew MyPoint3D(int::Parse(v[1]), int::Parse(v[2]), int::Parse(v[3]));
}
return TypeConverter::ConvertFrom(context, culture, value);
}
// Overrides the ConvertTo method of TypeConverter.
virtual Object^ ConvertTo(ITypeDescriptorContext^ context,
CultureInfo^ culture, Object^ value, Type^ destinationType) override {
if (destinationType == String::typeid) {
return value->ToString();
}
return TypeConverter::ConvertTo(context, culture, value, destinationType);
}
};
เมื่อสร้างเสร็จแล้ว สิ่งสำคัญคือ เราจะต้องลงทะเบียนให้ TypeDescriptor รู้ว่า จะต้องใช้ MyPoint3DConverter สำหรับ convert MyPoint3D ตอนเรียกใช้ TypeDescriptor::GetConverter() โดยขั้นตอนตรงนี้ ทาง Microsoft ได้ซ่อนกลไกการทำงานไว้ และให้เราทำโดยการระบุ TypeConverter Attribute ให้กับคลาส ผ่านทางการประกาศ attribute ที่หัวของคลาสดังนี้
ref class MyPoint3DConverter;
[TypeConverter(MyPoint3DConverter::typeid)]
ref class MyPoint3D
{
...
};
และเนื่องจาก การประกาศคลาส MyPoint3DConverter นั้น เราประกาศอยู่หลังคลาส MyPoint3D จึงทำให้เวลาคอมไพล์บรรทัดที่ประกาศ TypeConverter Attribute จะเกิด error ขึ้น เราจึงต้องเพิ่มบรรทัดที่ประกาศชื่อ MyPoint3DConverter ที่บรรทัดก่อนหน้ามันอีกทีหนึ่ง
สุดท้าย การใช้งาน
RegistryKey^ urk = Application::UserAppDataRegistry;
MyPoint3D^ a = gcnew MyPoint3D(12,13,14);
MyPoint3D^ b;
String^ stra = a->ToString(); // ได้ข้อความ "[12,13,14]"
String^ strb;
urk->SetValue("Test", a); // ปรากฏข้อความ "[12,13,14]" ใน registry
TypeConverter^ converter = TypeDescriptor::GetConverter(MyPoint3D::typeid);
String^ strvalue = (String^)urk->GetValue("Test");
if ((strvalue != nullptr) && (converter != nullptr)) {
b = (MyPoint3D^)converter->ConvertFromString(strvalue); // ได้ Object ใหม่ที่มีค่า x=12,y=13,z=14
strb = b->ToString(); // ได้ข้อความ "[12,13,14]"
}
สุดท้ายแล้ว หวังว่าบทความนี้จะทำให้ผู้ที่สนใจสามารถนำไปประยุกต์ใช้กับงานด้านอื่นๆได้อีกนะครับ