C语言基础教程:图形界面开发 (GUI)

本章我们将专注于 C 语言的 GUI 开发。 在前面的教程中,我们的程序大多像是一个“控制台战士”:你在小黑窗(终端)里输入一些数字或字符,程序在内存里经过复杂的计算,最后再把结果打印回小黑窗。

但现实世界中的软件,绝大多数都有漂亮的 图形用户界面 (GUI)。有窗口、有按钮、有菜单,用户可以通过鼠标点击来操作。是时候走出控制台了。

十多年前,我上学刚学C语言的时候,问老师如何绘制窗口和图像,老师很诚实,说她不会,因为她那些年也没有用C开发过有图形界面的项目。

虽然 C 语言本身并不包含图形库(标准库只管输入输出),但我们可以调用操作系统提供的 API,或者使用强大的第三方库来实现。


我们将通过实现同一个功能——完整的四则运算计算器,来分别展示 Windows 和 Linux 下不同的 GUI 开发方式。

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

如果你在 Windows 上开发,操作系统本身就提供了一套庞大的函数库,叫做 Windows API(也叫 Win32 API)。通过它,你可以控制 Windows 的一切。

1.1 设计思路

Windows 编程的核心是 消息循环 (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 编译时,需要链接 user32gdi32 库。另外,为了防止中文乱码警告,建议指定字符集。

clang win32_calc.c -o win32_calc.exe -luser32 -lgdi32 -finput-charset=UTF-8
  • -luser32 -lgdi32: 链接 Windows 系统库(虽然代码中加了 #pragma,但显式指定更稳妥)。
  • -finput-charset=UTF-8: 告诉编译器源码是 UTF-8 编码,解决中文乱码警告。

运行后,你会看到一个包含完整键盘的计算器窗口。因为 Win32 没有布局管理器,我们必须在代码中手动计算每个按钮的 x, y 坐标 (例如 10 + col * 60)。


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

原生 Windows API 虽然强大,但代码无法在 Linux 上运行。如果你想开发跨平台的 GUI 应用,或者专注于 Linux 桌面开发,GTK (GIMP Toolkit) 是标准选择之一。

2.1 安装开发环境 (Ubuntu)

在 Ubuntu 下,安装 GTK 开发库非常简单:

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

2.2 设计思路

GTK 使用 布局管理器 来排列控件,这比 Win32 的绝对坐标要灵活得多。

我们将使用: * 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 编译与运行

使用 pkg-config 工具来自动生成编译参数。

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

运行 ./gtk_calc,你会看到一个风格现代、会自动调整布局的窗口。与 Win32 不同,这里的 gtk_grid_attach 自动处理了行列对齐,无需计算像素。


总结

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

对于初学者,GTK 的逻辑通常更容易理解,而 Win32 API 则是理解 Windows 操作系统底层运作的途径。

Category: C
Category
Tagcloud
Hack RTL-SDR Lens SKill Book GPT-OSS Discuss Poem OpenWebUI Mac Conda FckZhiHu Python Junck Translate Data Virtual Machine OpenCL Communicate Shit Kivy IDE Software VTK Tape Programming MayaVi Chat Memory Cursor Learning Remote VM Prompt Nvidia Photo Code Game Hardware Raspbian AI,Data Science Science Microscope GlumPy 耳机 LTO Story Qwen3 Geology LlamaFactory Code Generation Radio C Windows Camera HBase QGIS Tools Ollama Windows11 Agent Hadoop Linux FuckZhihu n8n Translation ML Ubuntu AI CUDA PVE Video GIS RaspberryPi Photography ChromeBook History Hackintosh Virtualization PyOpenCL AIGC Mount&Blade 音频 SandBox PHD VirtualMachine TUNA Pyenv Visualization Tool macOS NixOS FuckChunWan Scholar 蓝牙 Life LLM University QEMU LTFS