I'm trying to figure out how to draw piano keys in GTK+3 sharp.
What is the best way to create it in Visual Studio?
Thanks
I'm trying to figure out how to draw piano keys in GTK+3 sharp.
What is the best way to create it in Visual Studio?
Thanks
I was not really sure what you needed but since I have an implementation of a keyboard for my C project it might help you a bit! I will post the code and my explanations here!
This implementation draws a keyboard for n octaves (the parameter you can specify with the GtkScale on the left of the application), and when a key on the piano is pressed it will highlight it in grey.
Please feel free to ask any questions if there is a part that you do not understand!
Generalities
Method
Here is how I have approached the problem!
Draw the keyboard Let us look at an octave of a piano keyboard, we can clearly see that there are 4 different types of keys: The right type (Do,Fa), The center type (Re,Sol,La), The left type (Mi,Si) and The Black keys. So we need to be able to draw all four of those types, if we want to than redraw a specific key in a different color when it is pressed.
Track the user cursor I have already said that we will use the GtkEventBox for this purpose, but once we get the (x,y) coordinates of the cursor, how do we know to which key area that coordinate corresponds? The trouble here is that only the black keys can be seen as a simple rectangle, the others are actually made up of two. So we need a general purpose function that will verify if we have clicked i a particular rectangle area!
Code
Keeping all the previous ideas in mind, let's get to the actual code!
Glade Here is the .glade of the piano, let's take a quick look at it and understand what it does.
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.38.2 -->
<interface>
<requires lib="gtk+" version="3.24"/>
<object class="GtkAdjustment" id="adjustment1">
<property name="lower">1</property>
<property name="upper">7</property>
<property name="value">1</property>
<property name="step-increment">1</property>
<property name="page-increment">10</property>
</object>
<object class="GtkWindow" id="wind">
<property name="can-focus">False</property>
<property name="default-width">480</property>
<property name="default-height">320</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<child>
<object class="GtkScale" id="octaves">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="orientation">vertical</property>
<property name="adjustment">adjustment1</property>
<property name="inverted">True</property>
<property name="round-digits">1</property>
<property name="digits">0</property>
<property name="value-pos">bottom</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkEventBox" id="event_box">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="above-child">True</property>
<child>
<object class="GtkDrawingArea" id="da">
<property name="visible">True</property>
<property name="can-focus">False</property>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
</object>
</interface>
First you can see a GtkAdjustment object, this will be used for the GtkScale that will allow you to choose how many octaves you want your piano to have. There is a main GtkWindow whose chile is a GtkBox that has two children, the GtkScale and the GtkEventBox. As said previously the child of the GtkEventBox is GtkDrawingArea
Code Now let's talk about the code
#include <stdlib.h>
#include <stdio.h>
#include <gtk/gtk.h>
int x;
int y;
int octave_number;
//returns 1 is the (currentx,current_y) is in the rectangle else 0
int is_in_rectangle(int current_x, int current_y, int rect_top_x, int rect_top_y, int rect_width, int rect_height)
{
return (current_x < rect_top_x + rect_width && current_y < rect_top_y + rect_height && current_x > rect_top_x && current_y > rect_top_y) ? 1 : 0;
}
//Sets the new octave value
static gboolean
on_scale_change(GtkWidget *a_scale, __attribute_maybe_unused__ gpointer user_data)
{
int new_size = gtk_range_get_value(GTK_RANGE(a_scale));
// g_print("id: %d\n", id);
octave_number = new_size;
return G_SOURCE_REMOVE;
}
//Gets the current event and sets the x,y possition
static gboolean
current_key_click(GtkWidget *event_box, __attribute_maybe_unused__ gpointer user_data)
{
GdkEvent *event = gtk_get_current_event();
GdkDisplay *display = gdk_display_get_default();
GdkSeat *seat = gdk_display_get_default_seat(display);
GdkDevice *device = gdk_seat_get_pointer(seat);
if (gdk_event_get_event_type(event) == GDK_BUTTON_PRESS)
{
gdk_window_get_device_position(gtk_widget_get_window(GTK_WIDGET(event_box)), device, &x, &y, NULL);
}
if (gdk_event_get_event_type(event) == GDK_BUTTON_RELEASE)
{
x = -1;
y = -1;
}
gdk_event_free(event);
return G_SOURCE_REMOVE;
}
// General axes set_up
void set_up_axes(GdkWindow *window, GdkRectangle *da, cairo_t *cr, gdouble *clip_x1, gdouble *clip_y1, gdouble *clip_x2, gdouble *clip_y2, gdouble *dx, gdouble *dy)
{
gdk_window_get_geometry(window, &da->x, &da->y, &da->width, &da->height);
//Draw white background
cairo_set_source_rgb(cr, 1.0, 1.0, 1.0);
cairo_paint(cr);
cairo_device_to_user_distance(cr, dx, dy);
cairo_clip_extents(cr, clip_x1, clip_y1, clip_x2, clip_y2);
cairo_set_line_width(cr, *dx);
}
// Draws all the lines in one octave
static gboolean
on_draw_key_lines(cairo_t *cr, int drawing_area_width, int drawing_area_height, int num_octaves)
{
int num_keys = 7 * num_octaves;
for (int o = 0; o < num_octaves; o++)
{
for (size_t j = 0; j <= 7; j++)
{
int i= j+ o*7;
if (j == 7 || j ==3)
{
cairo_line_to(cr, drawing_area_width * i / num_keys, 0);
}
else
{
cairo_line_to(cr, drawing_area_width * i / num_keys, drawing_area_height * 3 / 5);
}
cairo_line_to(cr, drawing_area_width * i / num_keys, drawing_area_height);
cairo_set_source_rgb(cr, 0, 0, 0);
cairo_stroke(cr);
}
}
return G_SOURCE_REMOVE;
}
// Draws one black key
static gboolean
on_draw_black_key(cairo_t *cr, int drawing_area_width, int drawing_area_height, int num_keys, int j)
{
cairo_set_source_rgb(cr, 0, 0, 0);
int top_left_x = drawing_area_width * j / (num_keys * 4);
int top_right_x = drawing_area_width * (2 + j) / (num_keys * 4);
int bot_right_y = drawing_area_height * 3 / 5;
cairo_line_to(cr, top_left_x, 0);
cairo_line_to(cr, top_right_x, 0);
cairo_line_to(cr, top_right_x, bot_right_y);
cairo_line_to(cr, top_left_x, bot_right_y);
cairo_line_to(cr, top_left_x, 0);
if (is_in_rectangle(x, y, top_left_x, 0, top_right_x - top_left_x, bot_right_y))
{
cairo_set_source_rgb(cr, 0.5, 0.5, 0.5);
}
cairo_fill(cr);
return G_SOURCE_REMOVE;
}
// Draw all the balck keys in one octave
static gboolean
on_draw_black_keys(cairo_t *cr, int drawing_area_width, int drawing_area_height, int num_octaves)
{
int num_keys = 7 * num_octaves;
for (int o = 0; o < num_octaves; o++)
{
for (size_t i = 3; i < 28; i += 4)
{
int j = i + o * 28;
if (i != 11 && i != 27)
{
on_draw_black_key(cr, drawing_area_width, drawing_area_height, num_keys, j);
}
}
}
return G_SOURCE_REMOVE;
}
//Draws one left type white key
static gboolean
on_draw_left_type_white_key(cairo_t *cr, int drawing_area_width, int drawing_area_height, int num_keys, int j)
{
// Default color if not pressed
cairo_set_source_rgb(cr, 1, 1, 1);
// The origin from which the tracing starts
int origin = drawing_area_width / (num_keys / 7) * j / 7;
// parameters of the top rectangle
int top_rect_width = drawing_area_width * 3 / (num_keys * 4);
int top_rect_height = drawing_area_height * 3 / 5;
// parametes of the bottom rectangle
int bot_rect_width = drawing_area_width / num_keys;
int bot_rect_height = drawing_area_height * 2 / 5;
// Draw the top part
cairo_line_to(cr, origin, 0);
cairo_line_to(cr, top_rect_width + origin, 0);
cairo_line_to(cr, top_rect_width + origin, top_rect_height);
// Check if the key is pressed on the top part
if (is_in_rectangle(x, y, origin, 0, top_rect_width, top_rect_height))
{
cairo_set_source_rgb(cr, 0.5, 0.5, 0.5);
}
// Draw the bottom part
cairo_line_to(cr, bot_rect_width + origin, top_rect_height);
cairo_line_to(cr, bot_rect_width + origin, drawing_area_height);
cairo_line_to(cr, origin, drawing_area_height);
cairo_line_to(cr, origin, 0);
// Check if the key is pressed on the bottom part
if (is_in_rectangle(x, y, origin, top_rect_height, bot_rect_width, bot_rect_height))
{
cairo_set_source_rgb(cr, 0.5, 0.5, 0.5);
}
// Draw the rectangle
cairo_fill(cr);
return G_SOURCE_REMOVE;
}
//Draws all the white keys
static gboolean
on_draw_left_type_white_keys(cairo_t *cr, int drawing_area_width, int drawing_area_height, int num_octaves)
{
int num_keys = 7 * num_octaves;
for (int o = 0; o < num_octaves; o++)
{
on_draw_left_type_white_key(cr, drawing_area_width, drawing_area_height, num_keys, 0 + o * 7);
on_draw_left_type_white_key(cr, drawing_area_width, drawing_area_height, num_keys, 3 + o * 7);
}
return G_SOURCE_REMOVE;
}
//Draws one center type white key
static gboolean
on_draw_center_type_white_key(cairo_t *cr, int drawing_area_width, int drawing_area_height, int num_keys, int j)
{
// Default color if not pressed
cairo_set_source_rgb(cr, 1, 1, 1);
// The tracing origin
int origin = drawing_area_width / (num_keys / 7) * j / 7 + drawing_area_width / (num_keys * 4);
// Top rectangle parameters
int top_rect_width = drawing_area_width / (num_keys * 2);
int top_rect_height = drawing_area_height * 3 / 5;
// parametes of the bottom rectangle
int bot_rect_width = origin - drawing_area_width / (num_keys * 4);
int bot_rect_height = drawing_area_height * 2 / 5;
// Trace the top part
cairo_line_to(cr, origin, 0);
cairo_line_to(cr, origin + top_rect_width, 0);
cairo_line_to(cr, origin + top_rect_width, top_rect_height);
// Check if the key is pressed on the top part
if (is_in_rectangle(x, y, origin, 0, top_rect_width, top_rect_height))
{
cairo_set_source_rgb(cr, 0.5, 0.5, 0.5);
}
// Trace the bottom part
cairo_line_to(cr, drawing_area_width * 3 / (num_keys * 4) + origin, top_rect_height);
cairo_line_to(cr, drawing_area_width * 3 / (num_keys * 4) + origin, drawing_area_height);
cairo_line_to(cr, bot_rect_width, drawing_area_height);
cairo_line_to(cr, bot_rect_width, top_rect_height);
cairo_line_to(cr, origin, top_rect_height);
cairo_line_to(cr, origin, 0);
// Check if the key is pressed on the bottom part
if (is_in_rectangle(x, y, bot_rect_width, top_rect_height, drawing_area_width / num_keys, bot_rect_height))
{
cairo_set_source_rgb(cr, 0.5, 0.5, 0.5);
}
cairo_fill(cr);
return G_SOURCE_REMOVE;
}
//Draws all center type white keys in an octave
static gboolean
on_draw_center_type_white_keys(cairo_t *cr, int drawing_area_width, int drawing_area_height, int num_octaves)
{
int num_keys = 7 * num_octaves;
for (int o = 0; o < num_octaves; o++)
{
on_draw_center_type_white_key(cr, drawing_area_width, drawing_area_height, num_keys, 1 + o * 7);
on_draw_center_type_white_key(cr, drawing_area_width, drawing_area_height, num_keys, 4 + o * 7);
on_draw_center_type_white_key(cr, drawing_area_width, drawing_area_height, num_keys, 5 + o * 7);
}
return G_SOURCE_REMOVE;
}
//Draws one right type white key
static gboolean
on_draw_right_type_white_key(cairo_t *cr, int drawing_area_width, int drawing_area_height, int num_keys, int j)
{
//Default color white
cairo_set_source_rgb(cr, 1, 1, 1);
//The origin of tracing
int origin = drawing_area_width / (num_keys / 7) * j / 7;
// parameters of the top rectangle
int top_rect_width = drawing_area_width * 3 / (num_keys * 4);
int top_rect_height = drawing_area_height * 3 / 5;
// parametes of the bottom rectangle
int bot_rect_width = drawing_area_width / num_keys;
int bot_rect_height = drawing_area_height * 2 / 5;
cairo_line_to(cr, origin, 0);
cairo_line_to(cr, origin, drawing_area_height);
cairo_line_to(cr, origin - bot_rect_width, drawing_area_height);
cairo_line_to(cr, origin - bot_rect_width, top_rect_height);
cairo_line_to(cr, origin - top_rect_width, top_rect_height);
cairo_line_to(cr, origin - top_rect_width, 0);
cairo_line_to(cr, origin, 0);
// Check if the key is pressed on the top part
if (is_in_rectangle(x, y, origin - top_rect_width, 0, top_rect_width, top_rect_height))
{
cairo_set_source_rgb(cr, 0.5, 0.5, 0.5);
}
// Check if the key is pressed on the bottom part
if (is_in_rectangle(x, y, origin - bot_rect_width , top_rect_height, bot_rect_width, bot_rect_height))
{
cairo_set_source_rgb(cr, 0.5, 0.5, 0.5);
}
cairo_fill(cr);
return G_SOURCE_REMOVE;
}
//Draws all right type white keys
static gboolean
on_draw_right_type_white_keys(cairo_t *cr, int drawing_area_width, int drawing_area_height, int num_octaves)
{
int num_keys = 7 * num_octaves;
for (int o = 0; o < num_octaves; o++)
{
on_draw_right_type_white_key(cr, drawing_area_width, drawing_area_height, num_keys, 3 + o * 7);
on_draw_right_type_white_key(cr, drawing_area_width, drawing_area_height, num_keys, 7 + o * 7);
}
return G_SOURCE_REMOVE;
}
//Draws the full keyboard
static gboolean
on_draw_full_keyboard(cairo_t *cr, int drawing_area_width, int drawing_area_height, int num_octaves)
{
// Draws all left type keys
on_draw_left_type_white_keys(cr, drawing_area_width, drawing_area_height, num_octaves);
// Draws all white type keys
on_draw_right_type_white_keys(cr, drawing_area_width, drawing_area_height, num_octaves);
// Draws all white center keys
on_draw_center_type_white_keys(cr, drawing_area_width, drawing_area_height, num_octaves);
// Draw the black keys
on_draw_black_keys(cr, drawing_area_width, drawing_area_height, num_octaves);
// Draw the lines for the keys
on_draw_key_lines(cr, drawing_area_width, drawing_area_height, num_octaves);
return G_SOURCE_REMOVE;
}
// Dynamically draws the signal
static gboolean
on_draw_signal(GtkWidget *widget, cairo_t *cr, __attribute_maybe_unused__ gpointer user_data)
{
GdkRectangle da; /* GtkDrawingArea size */
gdouble dx = 2.0, dy = 2.0; /* Pixels between each point */
gdouble clip_x1 = 0.0, clip_y1 = 0.0, clip_x2 = 0.0, clip_y2 = 0.0;
GdkWindow *window = gtk_widget_get_window(widget);
int drawing_area_width = gtk_widget_get_allocated_width(widget);
int drawing_area_height = gtk_widget_get_allocated_height(widget);
set_up_axes(window, &da, cr, &clip_x1, &clip_x2, &clip_y1, &clip_y2, &dx, &dy);
on_draw_full_keyboard(cr,drawing_area_width,drawing_area_height,octave_number);
gtk_widget_queue_draw_area(widget, 0, 0, drawing_area_width, drawing_area_height);
return G_SOURCE_REMOVE;
}
int main()
{
gtk_init(NULL, NULL);
x = y = -1;
octave_number = 1;
GtkBuilder *builder = gtk_builder_new();
GError *error = NULL;
if (gtk_builder_add_from_file(builder, "piano.glade", &error) == 0)
{
g_printerr("Error loading file: %s\n", error->message);
g_clear_error(&error);
return 1;
}
GtkWindow *window = GTK_WINDOW(gtk_builder_get_object(builder, "wind"));
GtkDrawingArea *da = GTK_DRAWING_AREA(gtk_builder_get_object(builder, "da"));
GtkEventBox *event_box = GTK_EVENT_BOX(gtk_builder_get_object(builder, "event_box"));
GtkScale *scale = GTK_SCALE(gtk_builder_get_object(builder, "octaves"));
g_signal_connect(G_OBJECT(window), "destroy", gtk_main_quit, NULL);
g_signal_connect(G_OBJECT(da), "draw", G_CALLBACK(on_draw_signal), NULL);
g_signal_connect(G_OBJECT(event_box), "event", G_CALLBACK(current_key_click), NULL);
g_signal_connect(G_OBJECT(scale), "value_changed", G_CALLBACK(on_scale_change), NULL);
gtk_widget_show_all(GTK_WIDGET(window));
gtk_main();
return 0;
}
The comments inside the code are pretty self-explanatory, but if you have any questions concerning a particular function, please do not hesitate to ask!
Here is what I am using to compile the code!
gcc -Wall -Wextra `pkg-config --cflags gtk+-3.0` -g -fsanitize=address main.c -o piano `pkg-config --libs gtk+-3.0`
I hope this was helpful! Best Regards,