找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

3238

积分

0

好友

444

主题
发表于 9 小时前 | 查看: 5| 回复: 0

你是否厌倦了始终在黑色的控制台窗口里与程序交互?诚然,强大的计算逻辑是核心,但在真实世界中,绝大多数软件都拥有直观的图形用户界面(GUI)。有窗口、有按钮,用户点点鼠标就能操作——这才是用户熟悉的体验。

十多年前我刚学C语言时,也曾好奇如何绘制窗口,老师的回答很实在:她当时也没有用C做过带图形界面的项目。这其实点明了一个关键:C语言的标准库专注于输入输出和基础算法,并不包含图形库。但别担心,我们可以通过调用操作系统提供的原生API,或者使用功能强大的第三方跨平台库来实现。

本文将通过一个具体且完整的项目——一个具备四则运算功能的计算器,带你分别探索在Windows和Linux下两种截然不同的GUI开发方式。这不仅是一份教程,更是一次直观的对比,让你理解不同技术栈的设计哲学和实现路径。

第一部分:Windows 原生开发 (Win32 API)

在Windows平台上开发,最直接的方式就是使用操作系统本身提供的一套庞大的函数库——Windows API(也称为Win32 API)。这套API允许你从底层控制窗口的创建、绘制、消息处理等几乎所有功能。

1.1 设计思路

Windows GUI编程的核心是 消息循环 (Message Loop)。系统会将用户的每一次点击、按键、移动鼠标等操作都封装成一个“消息”,应用程序的主循环则不断地获取并分发这些消息,由对应的窗口过程函数来处理。

为了实现一个完整的计算器,我们需要规划以下几个部分:

  • 历史记录栏 (Static Control):用于显示当前的计算过程,例如 12 + 5 *
  • 主显示屏 (Edit Control):显示当前输入的数字或最终的计算结果。
  • 完整键盘:包含数字0-9、小数点、四则运算符号以及清除键(C/CE)。
  • 逻辑处理:核心的算术运算、防止除以零、防止一个数字内输入多个小数点等。

1.2 代码实现

文件: code_examples/gui/win32_calc.c

#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// 自动链接库 (仅限 MSVC/Clang-cl)
#pragma comment(lib, "user32.lib")
#pragma comment(lib, "gdi32.lib")

// 全局控件句柄
HWND hDisplay;
HWND hHistory;

// 状态变量
double stored_value = 0.0;
char last_operator = 0;
BOOL new_entry = TRUE; // 标记是否开始输入新数字
BOOL has_decimal = FALSE; // 标记当前数字是否已有小数点

// 按钮布局定义 (5行4列)
const char *btn_labels[] = {
    "C", "CE", "", "/",  // 第一行: 清除全部, 清除当前, (空), 除
    "7", "8", "9", "*",  // 第二行
    "4", "5", "6", "-",  // 第三行
    "1", "2", "3", "+",  // 第四行
    "0", "", ".", "=" // 第五行: 0占两格(代码处理), 点, 等于
};

// 辅助函数:格式化双精度浮点数 (去除末尾多余0)
void format_double(char *buf, double val) {
    sprintf(buf, "%.10g", val);
}

// 辅助函数:执行计算
void calculate() {
    char buf[256];
    GetWindowText(hDisplay, buf, 256);
    double val = atof(buf);

    if (last_operator == '+') stored_value += val;
    else if (last_operator == '-') stored_value -= val;
    else if (last_operator == '*') stored_value *= val;
    else if (last_operator == '/') {
        if (val != 0) stored_value /= val;
        else {
            MessageBox(NULL, "除数不能为零!", "错误", MB_OK | MB_ICONERROR);
            stored_value = 0;
        }
    } else {
        stored_value = val;
    }

    // 显示结果
    format_double(buf, stored_value);
    SetWindowText(hDisplay, buf);
}

// 更新历史显示 (例如 "123 + ")
void update_history(double val, char op) {
    char buf[256];
    char num_buf[64];
    format_double(num_buf, val);

    if (op != 0 && op != '=') {
        sprintf(buf, "%s %c", num_buf, op);
    } else {
        // 如果是等号或重置,清空历史
        buf[0] = '\0';
    }
    SetWindowText(hHistory, buf);
}

// 窗口过程函数
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
    switch (uMsg) {
        case WM_CREATE: {
            // 字体
            HFONT hFontBtn = CreateFont(20, 0, 0, 0, FW_BOLD, FALSE, FALSE, FALSE, DEFAULT_CHARSET,
                                         OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, DEFAULT_QUALITY,
                                         DEFAULT_PITCH | FF_SWISS, "Arial");
            HFONT hFontDisplay = CreateFont(28, 0, 0, 0, FW_BOLD, FALSE, FALSE, FALSE, DEFAULT_CHARSET,
                                             OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, DEFAULT_QUALITY,
                                             DEFAULT_PITCH | FF_SWISS, "Arial");
            HFONT hFontHistory = CreateFont(16, 0, 0, 0, FW_NORMAL, FALSE, FALSE, FALSE, DEFAULT_CHARSET,
                                            OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, DEFAULT_QUALITY,
                                            DEFAULT_PITCH | FF_SWISS, "Arial");

            // 1. 历史记录 (Static Control, 右对齐)
            hHistory = CreateWindow("STATIC", "",
                WS_CHILD | WS_VISIBLE | SS_RIGHT,
                10, 10, 230, 20, hwnd, NULL, NULL, NULL);
            SendMessage(hHistory, WM_SETFONT, (WPARAM)hFontHistory, TRUE);

            // 2. 主显示屏 (Edit Control)
            hDisplay = CreateWindow("EDIT", "0",
                WS_CHILD | WS_VISIBLE | WS_BORDER | ES_RIGHT | ES_READONLY,
                10, 35, 230, 40, hwnd, NULL, NULL, NULL);
            SendMessage(hDisplay, WM_SETFONT, (WPARAM)hFontDisplay, TRUE);

            // 3. 创建按钮矩阵 (5行4列)
            int btn_w = 50;
            int btn_h = 40;
            int gap = 10;
            int start_y = 90;

            for (int i = 0; i < 20; i++) {
                if (strlen(btn_labels[i]) == 0 && i != 17) continue; // 跳过空标签,除了0旁边的空位(逻辑上0占两格)

                int row = i / 4;
                int col = i % 4;

                int x = 10 + col * (btn_w + gap);
                int y = start_y + row * (btn_h + gap);
                int w = btn_w;

                // 特殊处理 '0' 按钮,让它宽一点
                if (strcmp(btn_labels[i], "0") == 0) {
                    w = btn_w * 2 + gap; // 占两格
                } else if (i == 17) {
                    continue; // 0的右边一格被占用了,跳过创建
                }

                HWND hBtn = CreateWindow("BUTTON", btn_labels[i],
                    WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON,
                    x, y, w, btn_h,
                    hwnd, (HMENU)(100 + i), NULL, NULL);

                SendMessage(hBtn, WM_SETFONT, (WPARAM)hFontBtn, TRUE);
            }
            break;
        }

        case WM_COMMAND:
            if (LOWORD(wParam) >= 100 && LOWORD(wParam) < 120) {
                int btn_idx = LOWORD(wParam) - 100;
                const char *label = btn_labels[btn_idx];
                char buf[256];

                // 数字处理
                if ((label[0] >= '0' && label[0] <= '9')) {
                    if (new_entry) {
                        SetWindowText(hDisplay, label);
                        new_entry = FALSE;
                        has_decimal = FALSE;
                    } else {
                        GetWindowText(hDisplay, buf, 256);
                        // 简单防止溢出
                        if (strlen(buf) < 16) {
                            // 如果是0且当前没有小数点,替换之
                            if (strcmp(buf, "0") == 0) SetWindowText(hDisplay, label);
                            else {
                                strcat(buf, label);
                                SetWindowText(hDisplay, buf);
                            }
                        }
                    }
                }
                // 小数点
                else if (label[0] == '.') {
                    if (new_entry) {
                        SetWindowText(hDisplay, "0.");
                        new_entry = FALSE;
                        has_decimal = TRUE;
                    } else if (!has_decimal) {
                        GetWindowText(hDisplay, buf, 256);
                        strcat(buf, ".");
                        SetWindowText(hDisplay, buf);
                        has_decimal = TRUE;
                    }
                }
                // 清除全部 (C)
                else if (strcmp(label, "C") == 0) {
                    stored_value = 0.0;
                    last_operator = 0;
                    new_entry = TRUE;
                    has_decimal = FALSE;
                    SetWindowText(hDisplay, "0");
                    SetWindowText(hHistory, "");
                }
                // 清除当前 (CE)
                else if (strcmp(label, "CE") == 0) {
                    SetWindowText(hDisplay, "0");
                    new_entry = TRUE;
                    has_decimal = FALSE;
                }
                // 等号处理
                else if (label[0] == '=') {
                    calculate();
                    SetWindowText(hHistory, ""); // 清空历史
                    last_operator = 0;
                    new_entry = TRUE;
                }
                // 运算符处理 (+, -, *, /)
                else if (strlen(label) > 0) { // 排除空按钮
                    char current_text[256];
                    GetWindowText(hDisplay, current_text, 256);
                    double current_val = atof(current_text);

                    if (last_operator != 0 && !new_entry) {
                        calculate();
                        // calculate 更新了 stored_value
                    } else {
                        stored_value = current_val;
                    }

                    last_operator = label[0];
                    update_history(stored_value, last_operator);
                    new_entry = TRUE;
                }
            }
            break;

        case WM_DESTROY:
            PostQuitMessage(0);
            return 0;
    }
    return DefWindowProc(hwnd, uMsg, wParam, lParam);
}

int main() {
    WNDCLASS wc = {0};
    wc.lpfnWndProc = WindowProc;
    wc.hInstance = GetModuleHandle(NULL);
    wc.lpszClassName = "CalcWin32";
    wc.hbrBackground = (HBRUSH)(COLOR_WINDOW+1);
    wc.hCursor = LoadCursor(NULL, IDC_ARROW);
    RegisterClass(&wc);

    // 调整窗口大小
    CreateWindow("CalcWin32", "Win32 计算器",
        WS_OVERLAPPEDWINDOW | WS_VISIBLE & ~WS_MAXIMIZEBOX,
        100, 100, 265, 380, NULL, NULL, wc.hInstance, NULL);

    MSG msg;
    while (GetMessage(&msg, NULL, 0, 0)) {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }
    return 0;
}

1.3 编译与运行

如果你使用Clang或GCC(例如MinGW)在Windows上编译,需要显式链接 user32gdi32 这两个系统库。为了避免中文源码可能产生的警告,可以指定输入字符集。

clang win32_calc.c -o win32_calc.exe -luser32 -lgdi32 -finput-charset=UTF-8
  • -luser32 -lgdi32: 链接Windows GUI核心库。
  • -finput-charset=UTF-8: 告知编译器源代码采用UTF-8编码,避免中文字符警告。

运行程序后,一个功能完整的计算器窗口就会出现。这里有一个关键点:由于Win32 API没有自动的布局管理器,每个按钮的 (x, y) 坐标都需要我们在代码中手动计算(例如公式 10 + col * 60),这虽然繁琐,但带来了对界面像素级的精确控制。

Win32 API计算器运行界面

第二部分:Linux (Ubuntu) 下的 GTK 开发

原生Windows API虽然强大高效,但代码无法直接移植到其他操作系统。如果你希望开发跨平台的桌面应用,或者专注于Linux环境,GTK (GIMP Toolkit) 是一个历史悠久且功能完善的选择。

2.1 安装开发环境 (Ubuntu)

在基于Debian/Ubuntu的系统上,安装GTK开发库非常便捷:

sudo apt update
sudo apt install libgtk-3-dev -y

2.2 设计思路

与Win32的直接坐标控制不同,GTK推崇使用 布局管理器 来排列控件。这种方式更灵活,能自动适应窗口大小的变化,大大减轻了开发者的计算负担。

我们将使用以下核心控件:

  • GtkGrid: 网格布局,完美适配计算器键盘的排列。
  • GtkEntry: 用作主显示屏。
  • GtkLabel: 用于显示历史记录。
  • GtkButton: 各个功能按钮。

2.3 代码实现

文件: code_examples/gui/gtk_calc.c

#include <gtk/gtk.h>
#include <stdlib.h>
#include <string.h>

// 全局控件指针
GtkWidget *entry_display;
GtkWidget *label_history;

// 状态变量
double stored_value = 0.0;
char last_operator = 0;
gboolean new_entry = TRUE;
gboolean has_decimal = FALSE;

// 辅助函数:格式化显示
void format_double(char *buf, double val) {
    sprintf(buf, "%.10g", val);
}

// 更新历史显示
void update_history(double val, char op) {
    char buf[256];
    char num_buf[64];
    format_double(num_buf, val);

    if (op != 0 && op != '=') {
        sprintf(buf, "%s %c", num_buf, op);
    } else {
        buf[0] = '\0';
    }
    gtk_label_set_text(GTK_LABEL(label_history), buf);
}

// 执行计算
void calculate() {
    const char *text = gtk_entry_get_text(GTK_ENTRY(entry_display));
    double val = atof(text);

    if (last_operator == '+') stored_value += val;
    else if (last_operator == '-') stored_value -= val;
    else if (last_operator == '*') stored_value *= val;
    else if (last_operator == '/') {
        if (val != 0) stored_value /= val;
        else {
            gtk_entry_set_text(GTK_ENTRY(entry_display), "Error");
            new_entry = TRUE;
            return;
        }
    } else {
        stored_value = val;
    }

    char buf[64];
    format_double(buf, stored_value);
    gtk_entry_set_text(GTK_ENTRY(entry_display), buf);
}

// 按钮点击回调
static void on_button_clicked(GtkWidget *widget, gpointer data) {
    const char *label = gtk_button_get_label(GTK_BUTTON(widget));

    // 跳过空按钮
    if (strlen(label) == 0) return;

    // 数字键
    if (label[0] >= '0' && label[0] <= '9') {
        if (new_entry) {
            gtk_entry_set_text(GTK_ENTRY(entry_display), label);
            new_entry = FALSE;
            has_decimal = FALSE;
        } else {
            const char *current_text = gtk_entry_get_text(GTK_ENTRY(entry_display));
            if (strlen(current_text) < 16) {
                if (strcmp(current_text, "0") == 0) {
                    gtk_entry_set_text(GTK_ENTRY(entry_display), label);
                } else {
                    char *new_text = g_strdup_printf("%s%s", current_text, label);
                    gtk_entry_set_text(GTK_ENTRY(entry_display), new_text);
                    g_free(new_text);
                }
            }
        }
    }
    // 小数点
    else if (strcmp(label, ".") == 0) {
        if (new_entry) {
            gtk_entry_set_text(GTK_ENTRY(entry_display), "0.");
            new_entry = FALSE;
            has_decimal = TRUE;
        } else if (!has_decimal) {
            const char *current_text = gtk_entry_get_text(GTK_ENTRY(entry_display));
            char *new_text = g_strdup_printf("%s.", current_text);
            gtk_entry_set_text(GTK_ENTRY(entry_display), new_text);
            g_free(new_text);
            has_decimal = TRUE;
        }
    }
    // 清除全部 (C)
    else if (strcmp(label, "C") == 0) {
        stored_value = 0.0;
        last_operator = 0;
        new_entry = TRUE;
        has_decimal = FALSE;
        gtk_entry_set_text(GTK_ENTRY(entry_display), "0");
        gtk_label_set_text(GTK_LABEL(label_history), "");
    }
    // 清除当前 (CE)
    else if (strcmp(label, "CE") == 0) {
        gtk_entry_set_text(GTK_ENTRY(entry_display), "0");
        new_entry = TRUE;
        has_decimal = FALSE;
    }
    // 等号
    else if (strcmp(label, "=") == 0) {
        calculate();
        gtk_label_set_text(GTK_LABEL(label_history), "");
        last_operator = 0;
        new_entry = TRUE;
    }
    // 运算符
    else {
        const char *text = gtk_entry_get_text(GTK_ENTRY(entry_display));
        double current_val = atof(text);

        if (last_operator != 0 && !new_entry) {
            calculate();
        } else {
            stored_value = current_val;
        }

        last_operator = label[0];
        update_history(stored_value, last_operator);
        new_entry = TRUE;
    }
}

int main(int argc, char *argv[]) {
    gtk_init(&argc, &argv);

    // 1. 创建主窗口
    GtkWidget *window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
    gtk_window_set_title(GTK_WINDOW(window), "GTK3 Calculator");
    gtk_container_set_border_width(GTK_CONTAINER(window), 10);
    g_signal_connect(window, "destroy", G_CALLBACK(gtk_main_quit), NULL);

    // 2. 垂直布局 (History + Display + Grid)
    GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 5);
    gtk_container_add(GTK_CONTAINER(window), vbox);

    // 3. 历史记录 (Label)
    label_history = gtk_label_new("");
    gtk_label_set_xalign(GTK_LABEL(label_history), 1.0); // 右对齐
    gtk_box_pack_start(GTK_BOX(vbox), label_history, FALSE, FALSE, 0);

    // 4. 显示屏 (Entry)
    entry_display = gtk_entry_new();
    gtk_entry_set_alignment(GTK_ENTRY(entry_display), 1.0); // 右对齐
    gtk_entry_set_text(GTK_ENTRY(entry_display), "0");
    gtk_editable_set_editable(GTK_EDITABLE(entry_display), FALSE); // 只读
    gtk_box_pack_start(GTK_BOX(vbox), entry_display, FALSE, FALSE, 5);

    // 5. 按钮网格
    GtkWidget *grid = gtk_grid_new();
    gtk_grid_set_row_spacing(GTK_GRID(grid), 5);
    gtk_grid_set_column_spacing(GTK_GRID(grid), 5);
    gtk_box_pack_start(GTK_BOX(vbox), grid, TRUE, TRUE, 0);

    const char *labels[] = {
        "C", "CE", "", "/",
        "7", "8", "9", "*",
        "4", "5", "6", "-",
        "1", "2", "3", "+",
        "0", ".", "="
    };

    // 手动布局
    // Row 0
    GtkWidget *btn_c = gtk_button_new_with_label("C");
    g_signal_connect(btn_c, "clicked", G_CALLBACK(on_button_clicked), NULL);
    gtk_grid_attach(GTK_GRID(grid), btn_c, 0, 0, 1, 1);

    GtkWidget *btn_ce = gtk_button_new_with_label("CE");
    g_signal_connect(btn_ce, "clicked", G_CALLBACK(on_button_clicked), NULL);
    gtk_grid_attach(GTK_GRID(grid), btn_ce, 1, 0, 1, 1);

    // 空格占位 (2,0)

    GtkWidget *btn_div = gtk_button_new_with_label("/");
    g_signal_connect(btn_div, "clicked", G_CALLBACK(on_button_clicked), NULL);
    gtk_grid_attach(GTK_GRID(grid), btn_div, 3, 0, 1, 1);

    // Row 1-3 (Numbers)
    int num_idx = 4; // Start index in labels array
    for (int row = 1; row <= 3; row++) {
        for (int col = 0; col < 4; col++) {
            GtkWidget *btn = gtk_button_new_with_label(labels[num_idx++]);
            g_signal_connect(btn, "clicked", G_CALLBACK(on_button_clicked), NULL);
            gtk_widget_set_size_request(btn, 50, 40);
            gtk_grid_attach(GTK_GRID(grid), btn, col, row, 1, 1);
        }
    }

    // Row 4 (0, ., =)
    GtkWidget *btn_0 = gtk_button_new_with_label("0");
    g_signal_connect(btn_0, "clicked", G_CALLBACK(on_button_clicked), NULL);
    gtk_grid_attach(GTK_GRID(grid), btn_0, 0, 4, 2, 1); // 跨两列

    GtkWidget *btn_dot = gtk_button_new_with_label(".");
    g_signal_connect(btn_dot, "clicked", G_CALLBACK(on_button_clicked), NULL);
    gtk_grid_attach(GTK_GRID(grid), btn_dot, 2, 4, 1, 1);

    GtkWidget *btn_eq = gtk_button_new_with_label("=");
    g_signal_connect(btn_eq, "clicked", G_CALLBACK(on_button_clicked), NULL);
    gtk_grid_attach(GTK_GRID(grid), btn_eq, 3, 4, 1, 1);

    gtk_widget_show_all(window);
    gtk_main();

    return 0;
}

2.4 编译与运行

编译GTK程序需要链接正确的库和头文件路径,我们可以使用 pkg-config 工具来自动获取这些编译参数。

# 注意:pkg-config 命令被反引号 ` 包围
gcc gtk_calc.c -o gtk_calc `pkg-config --cflags --libs gtk+-3.0`

运行 ./gtk_calc,你会看到一个风格现代的计算器窗口。最大的区别在于:我们使用 gtk_grid_attach 将按钮“附着”到网格的行列位置上,而无需关心具体的像素坐标。窗口大小变化时,布局管理器会自动调整控件的位置和大小。

GTK计算器运行界面

总结与对比

通过实现同一个计算器项目,我们直观地体验了两种 GUI 开发模式的核心差异:

特性 Windows API (Win32) GTK+ (Linux/跨平台)
坐标系统 绝对坐标 (像素),需手动计算位置 布局管理 (Box/Grid),自动适应窗口大小
代码风格 过程式,Switch-Case 消息循环 面向对象风格,信号与回调 (Signal/Callback)
跨平台性 仅 Windows 跨平台 (Linux, Windows, macOS)
主要用途 底层系统工具,对性能和精细控制有极致需求 通用桌面应用,快速开发跨平台工具

对于初学者而言,GTK布局管理器和基于事件的回调模型通常更易于上手和理解。而学习 Win32 API,则是深入理解Windows操作系统图形子系统底层运作机制的绝佳途径。两者没有绝对的优劣,只有是否适合你的应用场景。

希望这篇实战指南能帮助你推开C语言图形界面开发的大门。从控制台到图形窗口,这一步的跨越会让你对程序与用户的交互有全新的认识。如果你想了解更多关于C语言底层原理、操作系统交互或其它开发技巧,欢迎在云栈社区继续探索和交流。




上一篇:C++未初始化变量陷阱:如何导致服务器定时任务崩溃
下一篇:尤雨溪谈 AI 开发 Vue 体验:Claude 4.6 框架级代码让创始人直呼“难懂”
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-2-26 16:41 , Processed in 0.420891 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表