เขียนแอปมอนิเตอร์ ping จอเดียวด้วย C++ ImGui กับ DirectX 11
เปิดหน้าต่าง ping ค้างไว้ยี่สิบบานแล้วไล่ดูทีละบานไม่ใช่การมอนิเตอร์ครับ มันคือการทรมานตัวเอง ผมเลยเขียนแอปที่ยิง ICMP เองทุกตัวพร้อมกันแล้วสรุปสถานะทั้งวงลงจอเดียว เปิดค้างไว้ข้างจอตอนไล่งานหน้างาน
สรุปใน 1 นาที
- อาการ/ปัญหา
- การเปิดหน้าต่าง ping ค้างไว้ยี่สิบบานแล้วไล่ดูทีละบานไม่ใช่การมอนิเตอร์
- ใครควรอ่าน
- คนดูแลเครือข่ายที่อยากเห็นสถานะอุปกรณ์ทั้งวงในจอเดียว
- ทางแก้แบบสั้น
- เขียนแอป C++ ที่ยิง ICMP เองผ่าน iphlpapi หนึ่ง thread ต่อหนึ่ง host คุยกับ UI ด้วย std::atomic วาดด้วย ImGui บน DirectX 11 และ build ด้วย CMake
พอดีงานเดินระบบเน็ตเวิร์กในอาคารหนึ่ง ผมต้องเฝ้าอุปกรณ์หลายสิบตัวพร้อมกันตอนไล่เก็บงาน ทั้ง AP ตามชั้น สวิตช์ กล้อง และเครื่องบันทึก ตอนแรกก็ใช้วิธีเปิด ping -t ค้างไว้หลายบานตามเคย พอเกินสิบตัวก็เริ่มไม่ไหว หน้าต่างบังกันเอง จะรู้ว่าตัวไหนหลุดต้องไล่คลิกดูทีละบาน แล้วพอกลับมาดูอีกทีก็ไม่รู้ว่ามันหลุดไปตอนไหน ผมเลยนั่งเขียนแอปตัวเล็กๆ ขึ้นมาตัวหนึ่ง เงื่อนไขคือต้องเปิดแล้วเห็นครบทั้งวงในจอเดียว ไม่ต้องติดตั้งอะไรเพิ่ม และเปิดค้างทั้งวันได้โดยไม่กินเครื่อง
เลข IP และชื่ออุปกรณ์ในบทความเป็นตัวอย่าง ส่วนโค้ดและโครงสร้างคือของจริงที่ใช้อยู่
โจทย์: เห็นสถานะทั้งวงในจอเดียว ไม่ต้องไล่เปิดทีละหน้าต่าง
สิ่งที่ผมอยากได้จริงๆ มีอยู่สี่ข้อ หนึ่งคือเห็นทุก IP พร้อมกันเรียงตามพอร์ตสวิตช์ให้ตรงกับที่เดินสายไว้ สองคือรู้ค่า RTT ปัจจุบันเป็นตัวเลข ไม่ใช่แค่เขียวแดง สามคือย้อนดูได้ว่าเมื่อกี้มันหลุดไปกี่ครั้ง และสี่คือคลิก IP แล้วเปิดหน้าเว็บของอุปกรณ์ได้เลยตอนจะเข้าไปแก้
ตัวเลขชุดนี้ผมตั้งมาเพื่อวงที่มี AP เยอะครับ ปลายทางเป็น WiFi ที่ตอบช้ากว่าสายอยู่แล้ว ถ้าไปตั้ง timeout 200 ms แบบยิงในวง LAN ล้วน จะได้แดงกระพริบทั้งจอโดยที่อุปกรณ์ไม่ได้เป็นอะไรเลย
ยิง ICMP เองด้วย iphlpapi ไม่เรียก ping.exe
ทางที่ง่ายที่สุดคือ CreateProcess เรียก ping.exe แล้วอ่านข้อความที่มันพ่นออกมา แต่ผมไม่เอาครับ เพราะสามสิบ host แปลว่าสร้าง process ใหม่สามสิบตัวทุกสองวินาที แล้วยังต้องมานั่งแกะข้อความที่เปลี่ยนตามภาษาของ Windows อีก
Windows มี API ให้ยิง ICMP ตรงอยู่แล้วใน iphlpapi.dll ใช้แค่สามฟังก์ชัน เปิด handle ด้วย IcmpCreateFile() ยิงด้วย IcmpSendEcho() แล้วปิดด้วย IcmpCloseHandle() ค่า RTT มันคืนมาให้ในโครงสร้าง ICMP_ECHO_REPLY เรียบร้อย ไม่ต้องแกะข้อความ ไม่ต้องขอสิทธิ์ raw socket และไม่ต้องรัน as administrator
ง่ายตอนเขียน แต่จ่ายแพงตอนรัน
สร้าง process ใหม่ทุกรอบทุก host, ต้อง parse ข้อความที่เปลี่ยนตามภาษา, หน้าต่าง console เด้งถ้าลืมซ่อน, ยกเลิกกลางคันยาก
คุยกับ stack ตรงๆ
ไม่มี process ใหม่, ได้ RTT เป็นตัวเลขมาเลย, กำหนด timeout ต่อครั้งได้, ไม่ต้องเป็น admin เพราะ Windows จัดการ raw socket ให้ข้างใน
โครงของ worker ตัวหนึ่งหน้าตาแบบนี้ครับ เตรียม handle กับ buffer ครั้งเดียวนอกลูป แล้วในลูปเหลือแค่ยิงกับอ่านผล
#include <iphlpapi.h>
#include <icmpapi.h>
#pragma comment(lib, "iphlpapi.lib")
static void PingLoop(HostState* hs)
{
HANDLE hIcmp = IcmpCreateFile();
if (hIcmp == INVALID_HANDLE_VALUE) return;
IN_ADDR addr{};
if (InetPtonA(AF_INET, hs->ip.c_str(), &addr) != 1) {
IcmpCloseHandle(hIcmp);
return;
}
char sendData[8] = { 'u','i','p','i','n','g','\0','\0' };
const DWORD replySize = sizeof(ICMP_ECHO_REPLY) + sizeof(sendData) + 16;
std::vector<char> replyBuf(replySize);
while (g_run.load()) {
DWORD timeout = g_timeout_ms.load();
DWORD res = IcmpSendEcho(hIcmp, *(IPAddr*)&addr,
sendData, sizeof(sendData), nullptr,
replyBuf.data(), replySize, timeout);
bool ok = (res > 0);
uint32_t rtt = 0;
if (ok) {
auto* rep = reinterpret_cast<ICMP_ECHO_REPLY*>(replyBuf.data());
rtt = rep->RoundTripTime;
}
hs->alive.store(ok);
hs->rtt_ms.store(rtt);
hs->last_epoch_ms.store(NowMs());
std::this_thread::sleep_for(std::chrono::milliseconds(g_interval_ms.load()));
}
IcmpCloseHandle(hIcmp);
}
จุดที่คนพลาดกันบ่อยคือขนาด buffer ครับ replySize ต้องเป็น sizeof(ICMP_ECHO_REPLY) + ขนาดข้อมูลที่ส่ง + 16 ตามที่เอกสารบอก ถ้าให้น้อยกว่านี้ IcmpSendEcho จะคืน 0 แล้วเราจะไปนั่งไล่หาว่าทำไมอุปกรณ์ทุกตัวตายหมด ทั้งที่ ping จาก cmd ยังได้อยู่
อีกอย่างคือต้อง #define WIN32_LEAN_AND_MEAN แล้ว include winsock2.h ก่อน windows.h เสมอ ไม่งั้นจะชนกับ winsock.h ตัวเก่าที่ windows.h ลากมาให้
หนึ่ง host หนึ่ง thread คุยกับ UI ผ่าน std::atomic
ผมเลือกวิธีที่ตรงที่สุดคือแตก thread ต่อ host ไปเลย ไม่ทำ thread pool ไม่ทำ async ครับ เพราะสามสิบกว่า thread ที่นอนหลับอยู่เกือบตลอดเวลาไม่ได้กินอะไรเลย ส่วน UI ที่วาดเฟรมละครั้งก็แค่อ่านค่าล่าสุดออกไปแสดง ไม่ต้องรอใคร
ตัวกลางระหว่างสองฝั่งคือ std::atomic ทุกตัวที่ worker เขียนแล้ว UI อ่าน
struct HostState {
std::string ip;
std::atomic<bool> alive{false};
std::atomic<uint32_t> rtt_ms{0};
std::atomic<uint64_t> last_epoch_ms{0};
// ตัวนับความนิ่ง กันสถานะกระพริบ
std::atomic<int> consec_ok{0};
std::atomic<int> consec_fail{0};
std::atomic<bool> stable_alive{false};
// ประวัติสำหรับกราฟ ตรงนี้เป็น vector เลยต้องมี mutex
struct Sample { uint64_t t_ms; float rtt; bool ok; };
std::mutex hist_mtx;
std::vector<Sample> history;
bool show_window = false;
};
static std::vector<std::unique_ptr<HostState>> g_hosts;
static std::vector<std::thread> g_threads;
static std::atomic<bool> g_run{false};
static std::atomic<uint32_t> g_interval_ms{2000};
static std::atomic<uint32_t> g_timeout_ms{800};
สังเกตว่า g_hosts เป็น vector<unique_ptr<HostState>> ไม่ใช่ vector<HostState> ครับ อันนี้ตั้งใจ เพราะ std::atomic กับ std::mutex ย้ายตำแหน่งไม่ได้ ถ้าเก็บเป็น object ตรงๆ แล้ววันหนึ่ง vector ขยายตัว มันจะพยายาม move ของทั้งก้อน แล้ว compile ไม่ผ่าน หรือแย่กว่านั้นคือ pointer ที่ worker ถืออยู่ชี้ไปที่หน่วยความจำเก่า การใส่ unique_ptr คั่นทำให้ที่อยู่ของ HostState นิ่งตลอดอายุโปรแกรม
การส่งค่ากลับใช้ store() กับ load() ธรรมดา ไม่ต้องล็อกอะไร ส่วนที่ยังต้องใช้ mutex มีที่เดียวคือ history เพราะเป็น vector ที่ทั้งเขียนและอ่านพร้อมกันจริง
Dear ImGui เป็น immediate mode ที่มี context ก้อนเดียวและไม่ thread-safe ทุกอย่างที่ขึ้นต้นด้วย ImGui:: ต้องอยู่ในลูปวาดของ thread หลักเท่านั้น worker มีหน้าที่แค่ store ค่าลง atomic แล้วจบ ถ้าเผลอไปเรียก ImGui::Text จาก worker อาการที่ได้คือแอปตายแบบสุ่ม หาสาเหตุไม่เจอ
กันไฟเขียวหลอกด้วยตัวนับติดกัน แล้วกระจาย jitter ไม่ให้ยิงพร้อมกัน
ปัญหาที่เจอตอนเปิดใช้จริงมีสองเรื่อง เรื่องแรกคือสถานะกระพริบ ping ตกครั้งเดียวจากสัญญาณ WiFi สะดุด จอก็แดงทั้งแถวแล้วเขียวกลับในวินาทีถัดไป มองไม่รู้เรื่อง ผมเลยเพิ่มสถานะที่สองชื่อ stable_alive ที่จะเปลี่ยนก็ต่อเมื่อผลออกมาทางเดียวกันติดกันครบสองรอบ ส่วนหลอดไฟบนจอใช้ค่าตัวนี้ ไม่ใช่ alive ดิบ
เรื่องที่สองคือ thundering herd ครับ ถ้าทุก thread สตาร์ตพร้อมกันแล้วนอนเท่ากันทุกรอบ มันจะยิงพร้อมกันเป็นชุดทุกสองวินาทีตลอดไป เห็นเป็นหนามบน NIC และทำให้ AP ตัวที่คิวยาวตอบช้าจนดูเหมือนมีปัญหา ทางแก้คือหน่วงต่อ host ไม่เท่ากัน แต่ต้องหน่วงคงที่ต่อ host ไม่ใช่สุ่มใหม่ทุกรอบ ผมเลยเอา IP ไป hash แบบ FNV-1a แล้วเอาเศษมาใช้เป็น jitter
// เกณฑ์เปลี่ยนสถานะ ต้องได้ผลทางเดียวกันติดกันครบก่อน
const int UP_TH = 2, DOWN_TH = 2;
if (ok) { hs->consec_ok.store(hs->consec_ok.load() + 1); hs->consec_fail.store(0); }
else { hs->consec_fail.store(hs->consec_fail.load() + 1); hs->consec_ok.store(0); }
bool st = hs->stable_alive.load();
if (!st && hs->consec_ok.load() >= UP_TH) st = true;
if ( st && hs->consec_fail.load() >= DOWN_TH) st = false;
hs->stable_alive.store(st);
// jitter คงที่ต่อ host จาก hash ของ IP กันยิงพร้อมกันทั้งวง
auto hash_ip = [](const std::string& s) {
uint32_t h = 2166136261u;
for (unsigned char c : s) { h ^= c; h *= 16777619u; }
return h;
};
uint32_t jitter = hash_ip(hs->ip) % 150; // 0..149 ms
std::this_thread::sleep_for(
std::chrono::milliseconds(g_interval_ms.load() + jitter));
เก็บประวัติเป็น ring buffer ที่คิดเพดานจากจำนวน host
ผมอยากย้อนดูได้ประมาณหนึ่งชั่วโมง แต่ไม่อยากให้แอปกินแรมไม่มีเพดานเวลาเปิดค้างข้ามคืน เลยคิดเพดานสองทางแล้วเอาค่าที่น้อยกว่า ทางแรกคือจำนวนตัวอย่างที่ครบหนึ่งชั่วโมงตามคาบปัจจุบัน ทางที่สองคือโควตาหน่วยความจำรวมหารด้วยจำนวน host
HostState::Sample smp{ NowMs(), ok ? (float)rtt : -1.0f, ok };
{
std::scoped_lock lk(hs->hist_mtx);
hs->history.push_back(smp);
uint32_t interval = g_interval_ms.load(); if (interval == 0) interval = 1;
size_t per_hour = (size_t)(3600ull * 1000ull / interval); // ครบ 1 ชม.
size_t total_cap = (size_t)((1ull << 30) / sizeof(HostState::Sample)); // เพดานรวม 1GB
size_t share = total_cap / std::max<size_t>(1, g_hosts.size());
size_t cap = std::min(per_hour, share);
while (hs->history.size() > cap) hs->history.erase(hs->history.begin());
}
Sample ตัวหนึ่ง 16 ไบต์ คาบ 2 วินาที แปลว่าหนึ่งชั่วโมงคือ 1800 ตัวอย่าง ราว 28 KB ต่อ host สามสิบ host ก็ไม่ถึงหนึ่งเมกะไบต์ เพดาน 1GB ไม่มีวันโดนแตะในการใช้งานปกติ มันมีไว้กันกรณีที่ใครไปตั้งคาบเป็น 200 ms แล้วโหลด IP มาเป็นร้อยตัว
ตรงนี้ยอมรับว่าใช้ erase(begin()) ซึ่งเป็น O(n) ทุกครั้งที่ล้น ของจริงควรเป็น std::deque หรือ ring buffer แบบ index วน แต่ที่ n ระดับพันกับความถี่ระดับวินาที มันวัดไม่ออก เลยยังไม่ได้แก้
วาดจอเดียวด้วย ImGui table
หัวใจของหน้าจอคือ BeginTable ก้อนเดียวที่มีสามกลุ่มคอลัมน์เรียงกันในแนวนอน กลุ่มละสี่คอลัมน์ คือหลอดสถานะ เลขห้อง IP และ RTT ทำแบบนี้แล้วแถวมันล็อกตรงกันทั้งจอ ไล่สายตาลงมาทีเดียวเห็นทั้งสวิตช์ตัวที่หนึ่ง ตัวที่สอง และฝั่งกล้อง
ImGuiTableFlags tf = ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg
| ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_Resizable
| ImGuiTableFlags_NoHostExtendX;
if (ImGui::BeginTable("SW_Combined", 12, tf)) {
ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, 16.0f);
ImGui::TableSetupColumn("ROOM", ImGuiTableColumnFlags_WidthFixed, 48.0f);
ImGui::TableSetupColumn("IP", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableSetupColumn("RTT", ImGuiTableColumnFlags_WidthFixed, 64.0f);
// ...อีกสองกลุ่มเหมือนกันเป๊ะ
ImGui::TableHeadersRow();
// ...วาดทีละแถว
ImGui::EndTable();
}
หลอดสถานะผมใช้ ImGui::ColorButton ขนาด 10x10 แทนที่จะวาดวงกลมเอง เพราะมันเป็น widget อยู่แล้ว เลยได้ hover ได้ click ได้ context menu ฟรี คลิกที่หลอดก็เปิดหน้าต่างกราฟของ host นั้น คลิกขวาก็ขึ้นสรุป loss กับ avg ทันที
ImGui::ColorButton(bulletId.c_str(), stable ? okCol : badCol,
ImGuiColorEditFlags_NoBorder | ImGuiColorEditFlags_NoTooltip,
ImVec2(10.0f, 10.0f));
if (ImGui::IsItemClicked() && hs) hs->show_window = !hs->show_window;
if (hs && ImGui::BeginPopupContextItem((std::string("ctx_") + ip).c_str())) {
size_t n = 0, okc = 0; float sum = 0, mn = 1e9f, mx = 0;
{
std::scoped_lock lk(hs->hist_mtx);
n = hs->history.size();
for (auto& s : hs->history)
if (s.ok) { okc++; sum += s.rtt; mn = std::min(mn, s.rtt); mx = std::max(mx, s.rtt); }
}
float loss = n ? 100.0f * (float)(n - okc) / (float)n : 0.0f;
ImGui::Text("loss=%.1f%% avg=%.1fms min=%.1f max=%.1f (n=%zu)",
loss, okc ? sum / okc : 0.0f, okc ? mn : 0.0f, okc ? mx : 0.0f, n);
if (ImGui::MenuItem("Show graph")) hs->show_window = true;
ImGui::EndPopup();
}
ช่อง IP ผมไม่ใช้ ImGui::Text ธรรมดา แต่วาง InvisibleButton ขนาดเท่าข้อความไว้รับคลิก แล้ววาดข้อความกับเส้นใต้เองด้วย draw list ตอน hover พอคลิกก็โยนให้ ShellExecuteA เปิดเบราว์เซอร์ไปหน้าเว็บของอุปกรณ์ กลายเป็นว่าจากจอเดียวนี้กดเข้าไปแก้ AP ตัวที่แดงได้เลย ไม่ต้องพิมพ์ IP ใหม่
std::string url = std::string("http://") + ip;
ImVec2 pos = ImGui::GetCursorScreenPos();
ImVec2 sz = ImGui::CalcTextSize(url.c_str());
ImGui::InvisibleButton((std::string("##ln_") + ip).c_str(), sz);
bool hov = ImGui::IsItemHovered();
ImDrawList* dl = ImGui::GetWindowDrawList();
ImU32 col = ImGui::ColorConvertFloat4ToU32(hov ? linkHoverCol : linkCol);
dl->AddText(pos, col, url.c_str());
if (hov) {
float y = pos.y + sz.y + 1.0f;
dl->AddLine(ImVec2(pos.x, y), ImVec2(pos.x + sz.x, y), col, 1.0f);
}
if (ImGui::IsItemClicked())
ShellExecuteA(NULL, "open", url.c_str(), NULL, NULL, SW_SHOWNORMAL);
กราฟใช้ ImGui::PlotLines ที่ติดมากับ ImGui เลย ไม่ต้องลง ImPlot เพิ่ม สำหรับดูว่าเมื่อกี้ RTT พุ่งไปกี่มิลลิวินาทีมันพอครับ
DirectX 11 backend กับแถบหัวหน้าต่างสีดำ
backend ผมเลือก DX11 เพราะบน Windows มันมาพร้อมเครื่องอยู่แล้ว ไม่ต้องพก DLL ของ GLFW หรือ SDL ไปด้วย ตัว swap chain ตั้งแค่ระดับพื้นฐานพอ
bool CreateDeviceD3D(HWND hWnd)
{
DXGI_SWAP_CHAIN_DESC sd{};
sd.BufferCount = 2;
sd.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
sd.OutputWindow = hWnd;
sd.SampleDesc.Count = 1;
sd.Windowed = TRUE;
sd.SwapEffect = DXGI_SWAP_EFFECT_DISCARD;
const D3D_FEATURE_LEVEL levels[2] = { D3D_FEATURE_LEVEL_11_0, D3D_FEATURE_LEVEL_10_0 };
D3D_FEATURE_LEVEL got;
if (D3D11CreateDeviceAndSwapChain(NULL, D3D_DRIVER_TYPE_HARDWARE, NULL, 0,
levels, 2, D3D11_SDK_VERSION, &sd,
&g_pSwapChain, &g_pd3dDevice, &got, &g_pd3dDeviceContext) != S_OK)
return false;
CreateRenderTarget();
return true;
}
สิ่งที่ห้ามลืมคือ WM_SIZE ต้องปล่อย render target view เก่าก่อนเรียก ResizeBuffers แล้วค่อยสร้างใหม่ ถ้าไม่ทำ พอย่อขยายหน้าต่างจะได้ภาพยืดหรือแอปดับ
case WM_SIZE:
if (g_pd3dDevice != nullptr && wParam != SIZE_MINIMIZED) {
CleanupRenderTarget();
g_pSwapChain->ResizeBuffers(0, (UINT)LOWORD(lParam), (UINT)HIWORD(lParam),
DXGI_FORMAT_UNKNOWN, 0);
CreateRenderTarget();
}
return 0;
อีกจุดที่เล็กแต่ทำให้ต่างกันมากคือแถบหัวหน้าต่าง ตัว ImGui เป็นธีมมืด แต่แถบหัวยังขาวอยู่ ดูแล้วขัดตา แก้ด้วย DwmSetWindowAttribute สองบรรทัด ต้องเซ็ตทั้ง attribute 20 กับ 19 เพราะ Windows 10 รุ่นก่อน 20H1 ใช้เลข 19 ส่วนรุ่นหลังใช้ 20 ยิงทั้งคู่ไปเลย ตัวที่ไม่รู้จักมันจะคืน error เฉยๆ ไม่พัง
#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE
#define DWMWA_USE_IMMERSIVE_DARK_MODE 20
#endif
#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE_BEFORE_20H1
#define DWMWA_USE_IMMERSIVE_DARK_MODE_BEFORE_20H1 19
#endif
static void EnableDarkTitleBar(HWND hwnd, bool enable)
{
BOOL val = enable ? TRUE : FALSE;
DwmSetWindowAttribute(hwnd, DWMWA_USE_IMMERSIVE_DARK_MODE, &val, sizeof(val));
DwmSetWindowAttribute(hwnd, DWMWA_USE_IMMERSIVE_DARK_MODE_BEFORE_20H1, &val, sizeof(val));
}
ต้องเรียกหลัง ShowWindow นะครับ ถ้าเรียกก่อนบางเครื่องจะไม่ติด
เรื่องเลย์เอาต์ ผมให้ ImGui เก็บ ini ของตัวเองไว้ในโฟลเดอร์ของแอป ไม่ปล่อยให้ไปตกใน working directory ที่รันมา จะได้จำขนาดหน้าต่างและตำแหน่งกราฟไว้ให้รอบหน้า
CreateDirectoryA("C:/net/uiping/config", NULL);
static char ini_path[MAX_PATH] = "C:/net/uiping/config/imgui.ini";
io.IniFilename = ini_path;
รายชื่อ IP ผมอ่านจากไฟล์ข้อความธรรมดา บรรทัดละหนึ่ง IP ขึ้นต้นด้วย # คือคอมเมนต์ ถ้าไม่มีไฟล์ก็ใช้ชุดสำรองที่ฝังไว้ จะได้เปิดแล้วเห็นอะไรบ้างไม่ใช่จอว่าง เพิ่มอุปกรณ์ใหม่ก็แก้ไฟล์ข้อความแล้วเปิดใหม่ ไม่ต้อง build ใหม่
# C:/net/uiping/ips.txt
192.168.10.11
192.168.10.12
192.168.10.21
# ชั้น 4 ยังไม่เดินสาย
192.168.10.41
192.168.10.51
build ด้วย CMake กับสคริปต์ PowerShell ตัวเดียว
ตัว CMakeLists ไม่มีอะไรพิสดาร ผมชี้ไปที่ซอร์สของ Dear ImGui ที่วางไว้ข้างนอกโปรเจกต์แล้วคอมไพล์รวมไปเลย ไม่ทำเป็น library แยก เพราะไฟล์มันแค่หกไฟล์
cmake_minimum_required(VERSION 3.24)
project(uiping LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(IMGUI_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../third_party/imgui)
set(IMGUI_SRC
${IMGUI_DIR}/imgui.cpp
${IMGUI_DIR}/imgui_draw.cpp
${IMGUI_DIR}/imgui_tables.cpp
${IMGUI_DIR}/imgui_widgets.cpp
${IMGUI_DIR}/backends/imgui_impl_win32.cpp
${IMGUI_DIR}/backends/imgui_impl_dx11.cpp)
add_executable(uiping WIN32 src/uiping_main.cpp ${IMGUI_SRC})
target_include_directories(uiping PRIVATE ${IMGUI_DIR} ${IMGUI_DIR}/backends)
target_link_libraries(uiping PRIVATE
d3d11 dxgi d3dcompiler dwmapi iphlpapi shell32 ws2_32)
add_definitions(-DUNICODE -D_UNICODE)
คำว่า WIN32 ใน add_executable คือตัวที่ทำให้ไม่มีหน้าต่าง console เด้งขึ้นมาคู่กับแอป (มันไปตั้ง subsystem เป็น WINDOWS แล้วใช้ wWinMain เป็นจุดเริ่ม) ส่วน library ที่ต้องมีครบคือ iphlpapi สำหรับ ICMP, dwmapi สำหรับแถบหัวมืด, shell32 สำหรับเปิดเบราว์เซอร์ และ ws2_32 สำหรับ InetPtonA
ที่ผมชอบใช้เวลาเขียนของแบบนี้บน Windows คือให้สคริปต์ PowerShell หา VS Build Tools เองแล้วเรียกตัวเองซ้ำในสภาพแวดล้อมที่ถูกต้อง จะได้ไม่ต้องเปิด Developer Command Prompt ทุกครั้ง
function Ensure-DevCmd() {
$cl = Get-Command cl -ErrorAction SilentlyContinue
$nmake = Get-Command nmake -ErrorAction SilentlyContinue
if ($cl -and $nmake) { return $true }
if ($Bootstrap) { return $true }
$roots = @('C:\Program Files (x86)\Microsoft Visual Studio\2022',
'C:\Program Files\Microsoft Visual Studio\2022')
$candidates = @()
foreach ($root in $roots) {
if (Test-Path $root) {
$candidates += Get-ChildItem -LiteralPath $root -Filter VsDevCmd.bat -Recurse `
-ErrorAction SilentlyContinue | Select-Object -ExpandProperty FullName
}
}
$vs = $candidates | Where-Object { Test-Path $_ } | Select-Object -First 1
if (-not $vs) { Die 'ไม่พบ VsDevCmd.bat ต้องติดตั้ง VS 2022 Build Tools ก่อน' }
# เรียกตัวเองซ้ำใน environment ที่ VsDevCmd ตั้งให้ (x64)
$ps = (Get-Command powershell).Source
$args = @('-ExecutionPolicy','Bypass','-File',"$PSCommandPath",'-Config',"$Config",'-Bootstrap')
cmd /c "call `"$vs`" -arch=x64 && `"$ps`" $($args -join ' ')"
exit $LASTEXITCODE
}
อีกท่าที่ช่วยได้จริงคือฆ่าโปรเซสเก่าก่อน build ครับ เพราะถ้าแอปยังเปิดค้างอยู่ ไฟล์ exe จะโดนล็อก linker เขียนทับไม่ได้ แล้ว error ที่ได้มันอ่านไม่รู้เรื่อง
$p = Get-Process uiping -ErrorAction SilentlyContinue
if ($p) { Stop-Process -Id $p.Id -Force; Start-Sleep -Milliseconds 200 }
if (Test-Path $Exe) {
$ok = $false
for ($i = 0; $i -lt 20; $i++) {
try { Remove-Item -Force $Exe; $ok = $true; break } catch { Start-Sleep -Milliseconds 200 }
}
if (-not $ok) { Die "ลบ $Exe ไม่ได้ (ไฟล์ถูกล็อก)" }
}
cmake -S $Proj -B $Build -G "NMake Makefiles" -DCMAKE_BUILD_TYPE=$Config
cmake --build $Build --config $Config
- วางซอร์ส Dear ImGui ไว้ที่ third_party/imgui (imgui.cpp, imgui_draw.cpp, imgui_tables.cpp, imgui_widgets.cpp และ backends win32 กับ dx11)
- รัน scripts/build.ps1 -Regen รอบแรกให้ CMake generate ด้วย NMake Makefiles
- รอบถัดไปรัน scripts/build.ps1 เฉยๆ มันจะ build อย่างเดียว ใช้เวลาไม่กี่วินาที
- แก้รายชื่ออุปกรณ์ที่ ips.txt แล้วเปิด build/uiping.exe ค้างไว้ข้างจอ
ของที่ยังไม่ได้ทำ
ที่ค้างอยู่ในหัวมีสามอย่างครับ อย่างแรกคือเขียน log ลงไฟล์ตอนสถานะเปลี่ยนพร้อม timestamp จะได้ย้อนดูข้ามวันได้ ไม่ใช่แค่หนึ่งชั่วโมงในแรม อย่างที่สองคือย้าย mapping ระหว่าง IP กับพอร์ตสวิตช์ที่ตอนนี้ยัง hardcode เป็น switch อยู่ในโค้ด ให้ไปอยู่ในไฟล์ข้อความข้างนอกเหมือน ips.txt อย่างที่สามคือแจ้งเตือนออก Telegram ตอนอุปกรณ์ตัวเดิมหลุดเกินเกณฑ์ จะได้ไม่ต้องนั่งเฝ้าจอ
ส่วนตัวผมคิดว่าเครื่องมือประเภทนี้ไม่ต้องสวยครับ ขอแค่เปิดแล้วรู้เรื่องภายในสามวินาที และไม่ต้องติดตั้งอะไรที่หน้างาน ตัวไฟล์ exe ตัวเดียวก๊อปใส่ไดรฟ์พกไปเสียบเครื่องลูกค้าได้เลย ใครจะเอาโครงนี้ไปต่อ เปลี่ยน backend เป็น OpenGL หรือเปลี่ยนจาก ICMP เป็น TCP connect ตามพอร์ตที่สนใจก็ทำได้ตามสะดวก แนวคิดของ worker กับ atomic ยังใช้ได้เหมือนเดิม ตรงไหนผมอธิบายตกหล่นหรือพิมพ์ผิด เดี๋ยวกลับมาแก้เพิ่มให้ภายหลังครับ
เคยเจองานแบบนี้ไหม? ติดตรงไหน?
มาแลกวิธีกันต่อในบอร์ด LinuxThai



ความคิดเห็น 0
ยังไม่มีความคิดเห็น — เป็นคนแรกเลย