本帖最后由 不爱胡萝卜的仓鼠 于 2023-12-26 13:32 编辑
之前跑helloWorld的时候绘制UI使用的就是GTK,那么我这次也使用GTK,本来想在帖子中记录一下我一步一步探索的情况,但是实际情况太复杂,这个GTK我是第一次上手,兜兜转转搞了搞几天,全写下来的话内容是在太多了,我这边只能简单讲一下demo的基本情况,最终我会把demo的源码贴出来,具体的请大家自行阅读代码。
在之前的几个帖子中,我已经掌握了基础的编译文件、下载运行代码、自动更新时间、获取天气信息等,基本上已经满足了制作demo的条件,现在就差学习一下如何使用GTK绘制界面了。(当然还有使用curl库从HTTPS下载天气的json数据、解析天气json数据。但是这两个我觉得还是很快可以上手的)
首先要去找一下GTK的官方文档,官网网址我放一下:https://www.gtk.org,我需要其中的API文档,连接如下:https://docs.gtk.org/gtk3/,这里面有快速上手的说明文档还有API说明。
说一下目标:全屏显示,显示当前年月日时分秒、显示天气预报(因为之前申请的是免费无限制接口,只能获取当前天气,反正大致意思到了就行)、一个刷新按钮,(用于手动刷新天气信息,当然代码也会定时自动刷新)、一个退出按钮(用于退出整个程序,因为全屏了,没有右上角的X了)
整个C代码如下
#include <gtk/gtk.h>
#include <glib.h>
#include <stdio.h>
#include <time.h>
#include <curl/curl.h>
#include <json-c/json.h>
#define FULLSCREEN (1)
/* 界面布局是否从UI文件加载 */
#define LOAD_UI_FROM_FILE (1)
#if !LOAD_UI_FROM_FILE
#define BOX (1)
#define GRID (2)
#define BOX_OR_GRID (BOX)
#endif
char weather_URL[] = "请求天气预报的https连接";
char outfilename[] = "weatherData.json";
void get_weather_data_and_refresh(GtkWidget *widget);
/* 更新时间的回调函数 */
gboolean update_time(GtkWidget *widget)
{
time_t current_time;
struct tm *time_info;
char time_string[30];
current_time = time(NULL);
time_info = localtime(¤t_time);
strftime(time_string, sizeof(time_string), "%Y-%m-%d %H:%M:%S", time_info);
gtk_label_set_text(GTK_LABEL(widget), time_string);
/* 返回true,继续执行定时器 */
return TRUE;
}
/* 首次运行更新天气 */
gboolean first_run_update_weather(GtkWidget *widget)
{
get_weather_data_and_refresh(widget);
/* 返回FALSE,不再继续执行定时器 */
return FALSE;
}
/* 更新天气 */
gboolean update_weather(GtkWidget *widget)
{
get_weather_data_and_refresh(widget);
/* 返回true,继续执行定时器 */
return TRUE;
}
/* 天气代码转英文字符 */
void weather_code_to_english_text(int code, char *text)
{
switch(code)
{
case 0:
{
strcpy(text, "Sunny");
}
break;
case 1:
{
strcpy(text, "Clear");
}
break;
case 2:
{
strcpy(text, "Fair");
}
break;
case 3:
{
strcpy(text, "Fair");
}
break;
case 4:
{
strcpy(text, "Cloudy");
}
break;
case 5:
{
strcpy(text, "Partly Cloudy");
}
break;
case 6:
{
strcpy(text, "Partly Cloudy");
}
break;
case 7:
{
strcpy(text, "Mostly Cloudy");
}
break;
case 8:
{
strcpy(text, "Mostly Cloudy");
}
break;
case 9:
{
strcpy(text, "Overcast");
}
break;
case 10:
{
strcpy(text, "Shower");
}
break;
case 11:
{
strcpy(text, "Thundershower");
}
break;
case 12:
{
strcpy(text, "Thundershower with Hail");
}
break;
case 13:
{
strcpy(text, "Light Rain");
}
break;
case 14:
{
strcpy(text, "Moderate Rain ");
}
break;
case 15:
{
strcpy(text, "Heavy Rain");
}
break;
case 16:
{
strcpy(text, "Storm");
}
break;
case 17:
{
strcpy(text, "Heavy Storm");
}
break;
case 18:
{
strcpy(text, "Severe Storm");
}
break;
case 19:
{
strcpy(text, "Ice Rain");
}
break;
case 20:
{
strcpy(text, "Sleet");
}
break;
case 21:
{
strcpy(text, "Snow Flurry ");
}
break;
case 22:
{
strcpy(text, "Light Snow");
}
break;
case 23:
{
strcpy(text, "Moderate Snow");
}
break;
case 24:
{
strcpy(text, "Heavy Snow");
}
break;
case 25:
{
strcpy(text, "Snowstorm");
}
break;
case 26:
{
strcpy(text, "Dust");
}
break;
case 27:
{
strcpy(text, "Sand");
}
break;
case 28:
{
strcpy(text, "Duststorm");
}
break;
case 29:
{
strcpy(text, "Sandstorm");
}
break;
case 30:
{
strcpy(text, "Foggy");
}
break;
case 31:
{
strcpy(text, "Haze");
}
break;
case 32:
{
strcpy(text, "Windy");
}
break;
case 33:
{
strcpy(text, "Blustery");
}
break;
case 34:
{
strcpy(text, "Hurricane");
}
break;
case 35:
{
strcpy(text, "Tropical Storm");
}
break;
case 36:
{
strcpy(text, "Tornado");
}
break;
case 37:
{
strcpy(text, "Cold");
}
break;
case 38:
{
strcpy(text, "Hot");
}
break;
default:
{
strcpy(text, "Unknown");
}
break;
}
}
/* 解析天气json数据,如果成功填充字符串 */
int parse_weather_json_data(char *weather_data)
{
char weather_text[30];
/* 从文件中解析JSON数据 */
json_object *parsed_json = json_object_from_file(outfilename);
/* 如果JSON解析失败 */
if (parsed_json == NULL)
{
printf("failed to parse weather json data\n");
sprintf(weather_data, "failed to parse weather data");
}
/* 解析成功 */
else
{
printf("success to parse JSON data\n");
// printf("weather json data: %s\n", json_object_to_json_string(parsed_json));
/* 找第一层results */
json_object *results = json_object_object_get(parsed_json, "results");
// printf("results: %s\n", json_object_get_string(results));
if (results != NULL)
{
for (int i = 0; i < json_object_array_length(results); i++)
{
/* results的数组的第i个 */
json_object *results_obj = json_object_array_get_idx(results, i);
/* 找到第二层的now */
json_object *now = json_object_object_get(results_obj, "now");
if (now != NULL)
{
/* 得到天气的code和温度数据 */
int code = json_object_get_int(json_object_object_get(now, "code"));
const char *temperature = json_object_get_string(json_object_object_get(now, "temperature"));
// printf("code: %d\n", code);
// printf("temperature: %s\n", temperature);
/* 天气code转成英文字符 */
weather_code_to_english_text(code, weather_text);
/* 组装整个天气的字符串 */
sprintf(weather_data, "now weather: %s now temperature: %s\n", weather_text, temperature);
}
else
{
printf("can't find now\n");
}
}
}
else
{
printf("can't find results\n");
}
/* 释放JSON对象内存 */
json_object_put(parsed_json);
printf("finish json parse\n");
}
return 0;
}
/* 回调函数,用于获取HTTP响应码 */
static int check_response(void *clientp, curl_infotype type, char *buffer, size_t size)
{
/* 暂时不需要响应码,屏蔽掉 */
#if 0
int response_code = 0;
if (type == CURLINFO_RESPONSE_CODE)
{
response_code = *((int *)clientp);
}
#endif
return 0;
}
/* 下载天气的json数据 */
int download_weather_json_data(void)
{
CURL *curl;
CURLcode res;
FILE *fp;
curl = curl_easy_init();
if (curl)
{
fp = fopen(outfilename, "wb");
if (!fp)
{
printf("Error opening file\n");
return 1;
}
curl_easy_setopt(curl, CURLOPT_URL, weather_URL);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, NULL); // 使用默认的写入函数
curl_easy_setopt(curl, CURLOPT_WRITEDATA, fp); // 将文件指针传递给写入函数
curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, NULL); // 不需要处理头部信息
curl_easy_setopt(curl, CURLOPT_HEADERDATA, NULL); // 不需要传递头部数据
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); // 不验证SSL证书(不安全,仅用于演示)
curl_easy_setopt(curl, CURLOPT_DEBUGFUNCTION, check_response); // 设置调试回调函数,用于获取HTTP响应码
res = curl_easy_perform(curl); // 发送请求并下载文件
if (res != CURLE_OK)
{
printf("curl_easy_perform() failed: %s\n", curl_easy_strerror(res));
fclose(fp); // 关闭文件指针
return 1;
}
else
{
printf("success download weather json data\n");
fclose(fp); // 关闭文件指针。
}
curl_easy_cleanup(curl); // 清理资源
}
else
{
printf("curl_easy_init() failed\n");
return 1;
}
return 0;
}
/* 获取天气数据并刷新 */
void get_weather_data_and_refresh(GtkWidget *widget)
{
char weather_string[50];
int result;
/* 获取天气JSON数据 */
result = download_weather_json_data();
/* 解析天气JSON数据 */
if (result == 1)
{
/* 下载天气json数据失败,显示对应的文本 */
sprintf(weather_string, "fail to download weather data");
gtk_label_set_text(GTK_LABEL(widget), weather_string);
}
else
{
sprintf(weather_string, "success to download weather data");
parse_weather_json_data(weather_string);
gtk_label_set_text(GTK_LABEL(widget), weather_string);
}
}
/* 刷新按钮回调函数,按钮被按下时调用 */
void button_refresh_clicked_cb(GtkButton *button, GtkWidget *widget)
{
gtk_label_set_text(GTK_LABEL(widget), "get weather data ......");
get_weather_data_and_refresh(widget);
}
/* mian函数 */
int main(int argc, char *argv[])
{
/* 初始化GTK */
gtk_init(&argc, &argv);
#if LOAD_UI_FROM_FILE
/* 从ui文件加载界面布局 */
GtkBuilder *builder = gtk_builder_new();
GError *error = NULL;
gtk_builder_add_from_file (builder, "/usr/local/ui.ui", &error);
if (error != NULL)
{
printf("error: fail to load UI file: %s\n", error->message);
g_error_free(error);
return 1;
}
#endif
/* 创建窗口 */
#if LOAD_UI_FROM_FILE
GtkWidget *window = GTK_WIDGET(gtk_builder_get_object(builder, "window"));
if (window == NULL)
{
printf("error: get object window fail\n");
}
#else
GtkWidget *window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
#endif
/* 设置窗口标签 */
gtk_window_set_title (GTK_WINDOW (window), "Clock and Weather Forecast");
/* 设置窗口的最小大小 */
gtk_widget_set_size_request(window, 400, 250);
/* 绑定窗口的X,可以从触摸屏关闭整个程序 */
g_signal_connect(G_OBJECT(window), "destroy", G_CALLBACK(gtk_main_quit), NULL);
#if !LOAD_UI_FROM_FILE
#if (BOX_OR_GRID == BOX)
/* 创建一个box,后续的控件都放box中 */
GtkWidget *box1 = gtk_box_new(GTK_ORIENTATION_VERTICAL, 20);
/* 把box放到window中 */
gtk_container_add(GTK_CONTAINER(window), box1);
#elif (BOX_OR_GRID == GRID)
/* 创建一个grid,后续的控件都放grid中 */
GtkWidget *grid = gtk_grid_new ();
/* 把grid放到window中 */
gtk_container_add (GTK_CONTAINER (window), grid);
#endif
#endif
/* 创建一个标签,用于显示时间 */
#if LOAD_UI_FROM_FILE
GtkWidget *label_timer = GTK_WIDGET(gtk_builder_get_object(builder, "label_timer"));
#else
GtkWidget *label_timer = gtk_label_new("waiting for get sys time");
#if (BOX_OR_GRID == BOX)
/* 把标签放进box中 */
gtk_box_pack_start(GTK_BOX(box1), label_timer, FALSE, FALSE, 0);
#elif (BOX_OR_GRID == GRID)
/* 把标签放进grid中 */
gtk_grid_attach (GTK_GRID (grid), label_timer, 0, 0, 3, 1);
#endif
#endif
/* 创建一个标签,用于天气信息 */
#if LOAD_UI_FROM_FILE
GtkWidget *label_weather = GTK_WIDGET(gtk_builder_get_object(builder, "label_weather"));
#else
GtkWidget *label_weather = gtk_label_new("waiting for get weather forecast information");
#if (BOX_OR_GRID == BOX)
/* 把标签放进box中 */
gtk_box_pack_start(GTK_BOX(box1), label_weather, FALSE, FALSE, 0);
#elif (BOX_OR_GRID == GRID)
/* 把标签放进grid中 */
gtk_grid_attach (GTK_GRID (grid), label_weather, 0, 1, 1, 1);
#endif
#endif
/* 创建一个button,用于手动刷新天气信息 */
#if LOAD_UI_FROM_FILE
GtkWidget *button_refresh = GTK_WIDGET(gtk_builder_get_object(builder, "button_refresh"));
#else
GtkWidget *button_refresh = gtk_button_new_with_label(refresh);
// gtk_widget_set_size_request(button_refresh, 50, 20);
#if (BOX_OR_GRID == BOX)
/* 把button放进box中 */
gtk_box_pack_start(GTK_BOX(box1), button_refresh, FALSE, FALSE, 0);
#elif (BOX_OR_GRID == GRID)
/* 把标签放进grid中 */
gtk_grid_attach (GTK_GRID (grid), button_refresh, 3, 1, 1, 1);
#endif
#endif
/* 设置回调函数 */
g_signal_connect(button_refresh, "clicked", G_CALLBACK(button_refresh_clicked_cb), label_weather);
/* 创建一个button,用于退出程序 */
#if LOAD_UI_FROM_FILE
GtkWidget *button_quit = GTK_WIDGET(gtk_builder_get_object(builder, "button_quit"));
#else
GtkWidget *button_quit = gtk_button_new_with_label("quit");
// gtk_widget_set_size_request(button_quit, 50, 20);
#if (BOX_OR_GRID == BOX)
/* 把button放进box中 */
gtk_box_pack_start(GTK_BOX(box1), button_quit, FALSE, FALSE, 0);
#elif (BOX_OR_GRID == GRID)
/* 把标签放进grid中 */
gtk_grid_attach (GTK_GRID (grid), button_quit, 3, 3, 1, 1);
#endif
#endif
/* 设置回调函数 */
g_signal_connect(button_quit, "clicked", G_CALLBACK (gtk_main_quit), NULL);
/* 把刚才创建的窗口显示出来 */
gtk_widget_show_all(window);
#if FULLSCREEN
/* 全屏显示 */
gtk_window_fullscreen(GTK_WINDOW(window));
#endif
/* 创建定时器,每隔1s刷新时间 */
g_timeout_add(1000, (GSourceFunc)update_time, label_timer);
/* 创建定时器,等时间到了,不再运行定时器。用于首次上电后更新天气数据。如果在这里直接更新,一旦下载或解析有问题,GTK界面会出来的很慢 */
g_timeout_add(1000, (GSourceFunc)first_run_update_weather, label_weather);
/* 创建定时器,30min定时更新一次天气数据 */
g_timeout_add(1800000, (GSourceFunc)update_weather, label_weather);
gtk_main();
#if LOAD_UI_FROM_FILE
g_object_unref(builder);
#endif
return 0;
}
配套的Makefile
PROG = gtk_time
SRCS = gtk_time.c
CLEANFILES = $(PROG)
# Add / change option in CFLAGS and LDFLAGS
CFLAGS += -Wall $(shell pkg-config --cflags gtk+-3.0)
LDFLAGS += $(shell pkg-config --libs gtk+-3.0)
CFLAGS += -I/path/to/curl/include
LDFLAGS += -L/path/to/curl/lib -lcurl
LDFLAGS += -L/path/to/json-c/lib -ljson-c
all: $(PROG)
$(PROG): $(SRCS)
$(CC) -o $@ $^ $(CFLAGS) $(LDFLAGS)
clean:
rm -f $(CLEANFILES) $(patsubst %.c,%.o, $(SRCS))
对应的ui.ui文件代码
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.2 -->
<interface>
<requires lib="gtk+" version="3.20"/>
<object class="GtkWindow" id="window">
<property name="can_focus">False</property>
<child type="titlebar">
<placeholder/>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkLabel" id="label_timer">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">waiting for get sys time</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="padding">32</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkLabel" id="label_weather">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="margin_left">38</property>
<property name="label" translatable="yes">waiting for get weather forecast information</property>
<property name="justify">center</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="button_refresh">
<property name="label" translatable="yes">refresh</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="pack_type">end</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="padding">41</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkButton" id="button_quit">
<property name="label" translatable="yes">quit</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="margin_left">170</property>
<property name="margin_right">170</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="pack_type">end</property>
<property name="position">2</property>
</packing>
</child>
</object>
</child>
</object>
</interface>
界面显示如下
这个代码写的界面有以下几种配置方式
①是否全屏显示,修改FULLSCREEN宏定义
②界面布局是否从UI文件加载,如果是,那么就需要把上面的ui.ui文件也放到135中,我代码中写的路径是/usr/local/。修改LOAD_UI_FROM_FILE宏定义
③不使用UI文件的情况下,使用BOX布局还是GRID布局,修改BOX_OR_GRID。这是我前期探索时,手动制作的界面布局
最终版的配置参数是全屏+使用UI加载
详细的代码我就不解读了,我写了非常详细的注释。
运行demo步骤如下,在ubuntu中编译好代码,将执行文件和ui.ui放到135的/usr/local下,修改两个文件的权限,然后运行
chmod 777 ui.ui
chmod 777 gtk_time
su -l weston -c "/usr/local/gtk_time"
我这里要说一下ui.ui文件是怎么来的,手动绘制界面真的非常麻烦,效率也很低,我在网上搜索到一个好东西“Glade”,他只管绘制界面,有点类似前端后端分离的感觉,他最终会生产一个文件,然后GTK加载这文件,就可以直接显示出绘制的界面,这个软件需要在ubuntu中运行(win貌似也可以,但是我没有找到对应的安装包),在ubuntu中安装只需要使用以下命令
sudo apt-get install glade
安装好后就有一个Glade应用程序的图标,点击即可运行(下图是我绘制的界面)
这个软件我就不细讲了,大家可以自行摸索,总的来说还是很容易上手的,我大约捣鼓了十来分钟就基本上手了
绘制好后,点击save,默认的文件扩展名是“.glade”,其实就是xml,可以手动修改后缀,然后就可以使用了(给到135后还要修改一下文件的权限,保证他可以被读取)
最后放一段演示视频
WeChat_20231224212942