如何在GTK4中通过鼠标移动事件旋转Cairo圆形音量按钮?
GTK4 Cairo圆形音量按钮问题修复
问题1:点击旋转跳转指针位置的修复
当前代码直接将鼠标位置的角度赋值给全局变量,导致点击任意位置时指示点直接跳到鼠标坐标对应的角度。解决核心是:
- 不再旋转整个画布,而是直接根据当前角度计算指示点位置,避免坐标混乱
- 限制角度在有效区间(对应音量1-100,取-90°到90°,匹配底部绿色指示线的范围)
- 仅响应圆形区域内的鼠标操作,避免误触
问题2:输出1-100整数数值的实现
通过线性映射将角度转换为目标区间:
- 角度范围:-90°(对应音量1)→ 90°(对应音量100)
- 计算公式:
volume = (角度 + 90) / 180 * 99 + 1,最终取整得到1-100的整数
修改后的完整代码
#include <gtk/gtk.h> #include <math.h> // 当前角度(范围:-90到90度,对应音量1到100) float current_angle = -90.0f; // 绘制圆形和音量指示点 static void draw_function(GtkDrawingArea *darea, cairo_t *cr, int width, int height, gpointer user_data) { int radius = MIN(width, height) / 2; int cx = width / 2; int cy = height / 2; // 绘制外圆 cairo_arc(cr, cx, cy, radius, 0.0, 2 * M_PI); cairo_set_source_rgb(cr, 0.6, 0.8, 1.0); cairo_fill_preserve(cr); cairo_set_source_rgb(cr, 0.0, 0.0, 0.0); cairo_stroke(cr); // 计算指示点位置 float rad = current_angle * M_PI / 180.0f; int dot_x = cx + (radius * 0.93) * cos(rad); int dot_y = cy + (radius * 0.93) * sin(rad); // 绘制音量指示点 cairo_arc(cr, dot_x, dot_y, radius * 0.05, 0.0, 2 * M_PI); cairo_set_source_rgb(cr, 0.9, 0.0, 1.0); cairo_fill_preserve(cr); cairo_set_source_rgb(cr, 0.0, 0.0, 0.0); cairo_stroke(cr); // 绘制起始和结束指示线 cairo_set_source_rgb(cr, 0, 1.0, 0); cairo_set_line_width(cr, 3.5); cairo_move_to(cr, 5, height - 5); cairo_line_to(cr, width * 13 / 100, height * 87 / 100); cairo_move_to(cr, width - 5, height - 5); cairo_line_to(cr, width * 87 / 100, height * 87 / 100); cairo_stroke(cr); } // 计算鼠标位置对应的角度,并限制范围 static float calculate_target_angle(int cx, int cy, double mx, double my) { float angle = atan2f(my - cy, mx - cx) * 180.0f / M_PI; // 限制角度在-90到90度之间(对应音量1到100) if (angle < -90.0f) angle = -90.0f; if (angle > 90.0f) angle = 90.0f; return angle; } // 判断鼠标是否在圆形区域内 static gboolean is_mouse_in_circle(int cx, int cy, int radius, double mx, double my) { double dx = mx - cx; double dy = my - cy; return (dx*dx + dy*dy) <= (radius*radius); } void pointer_motion_event(GtkEventController *gesture, gdouble x, gdouble y, gpointer user_data) { GdkModifierType state = gtk_event_controller_get_current_event_state(gesture); if (!(state & GDK_BUTTON1_MASK)) return; GtkWidget *darea = GTK_WIDGET(user_data); int width = gtk_widget_get_width(darea); int height = gtk_widget_get_height(darea); int cx = width / 2; int cy = height / 2; int radius = MIN(width, height) / 2; // 只在圆形区域内响应拖动 if (!is_mouse_in_circle(cx, cy, radius, x, y)) return; // 计算目标角度 current_angle = calculate_target_angle(cx, cy, x, y); // 转换为1-100的整数音量并输出 int volume = (int)((current_angle + 90.0f) / 180.0f * 99.0f + 1.0f); g_print("当前音量:%d\n", volume); gtk_widget_queue_draw(darea); } static void activate(GtkApplication* app, gpointer user_data) { GtkWidget *window = gtk_application_window_new(app); gtk_window_set_title(GTK_WINDOW(window), "圆形音量按钮"); gtk_window_set_default_size(GTK_WINDOW(window), 350, 350); GtkWidget *darea = gtk_drawing_area_new(); gtk_drawing_area_set_content_width(GTK_DRAWING_AREA(darea), 350); gtk_drawing_area_set_content_height(GTK_DRAWING_AREA(darea), 350); gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(darea), draw_function, NULL, NULL); gtk_window_set_child(GTK_WINDOW(window), darea); GtkEventController *motion_controller = gtk_event_controller_motion_new(); gtk_widget_add_controller(darea, motion_controller); g_signal_connect(motion_controller, "motion", G_CALLBACK(pointer_motion_event), darea); gtk_widget_show(window); } int main(int argc, char **argv) { GtkApplication *app = gtk_application_new("org.gtk.volume knob", G_APPLICATION_FLAGS_NONE); g_signal_connect(app, "activate", G_CALLBACK(activate), NULL); int status = g_application_run(G_APPLICATION(app), argc, argv); g_object_unref(app); return status; }
关键修改说明
- 移除Rotate函数:直接根据
current_angle计算指示点坐标,避免画布旋转带来的坐标偏移问题,同时提升绘制性能 - 角度范围限制:将角度锁定在-90°到90°,完美匹配底部绿色指示线的起止位置,对应音量1到100的区间
- 圆形区域判断:仅当鼠标在圆形按钮内部时才响应拖动,避免界面空白区域的误操作
- 音量数值转换:通过线性映射公式,精准将角度转换为1-100的整数,确保数值范围符合需求
- 拖动逻辑优化:拖动时平滑跟随鼠标位置(在有效范围内),不再出现点击跳转的问题
内容的提问来源于stack exchange,提问作者dibyendu
相关产品推荐
相关产品推荐

