This article reports on a revision to a tool that provides its users with the ability to pick colors from a linear color gradient using from two to up to six start/end colors. It also allows interpolation of the gradient.
Table of Contents
The symbol returns the reader to the top of the Table of Contents.
Introduction
This article revises an earlier revision of the Gradient Color Picker (V2) [^]. The incentive for the revision was a reader request for a larger number of initial color choices.
Two changes were implemented:
- The number of start/end buttons was to be increased.
- Interpolation was to be offered as a substitute for the Microsoft LinearGradientBrush [^]. The two algorithms that resulted are named LinearGradient and Interpolation.
Implementation
To the extent possible, the modifications required to achieve the desired changes were made with the goal of minimizing impacts on the existing software. However a number of inefficiencies were detected and removed during the process.
Increased number of start/end buttons
On initial execution, GradientColorPickerV3 displays the following:
Among other things, it invokes show_initial_GUI to create the initial User Interface (UI). The user specifies the number of start/end buttons by providing the desired number in the Start/End Buttons NumericUpDown (NUD) control. number_start_end_buttons records the NUD value. Each time that the NUD value changes, the display updates to show the new number of uncolored buttons.
show_initial_GUI invokes fill_button_panel.
fill_button_panel is invoked when either of the two button panels (start_end_buttons_PAN or selector_buttons_PAN) requires redrawing. (Note that class constants and variables are presented for completeness.)
const int BUTTON_HEIGHT = 30;
const int BUTTON_WIDTH = BUTTON_HEIGHT;
Size BUTTON_SIZE =
new Size ( BUTTON_WIDTH,
BUTTON_HEIGHT );
const int EPSILON = 3;
const int DEFAULT_NUMBER_START_END_BUTTONS = 6;
int number_start_end_buttons =
DEFAULT_NUMBER_START_END_BUTTONS;
List < Button > start_end_color_buttons =
new List < Button > ( );
bool show_initial_GUI ( )
{
start_end_buttons_LAB.Visible = true;
start_end_buttons_NUD.Visible = true;
error_message_RTB.Clear ( );
error_message_RTB.Visible = true;
start_end_buttons_PAN.Visible = true;
fill_button_panel ( number_start_end_buttons,
start_end_buttons_PAN,
start_end_color_buttons,
start_end_button_Click );
return ( true );
}
bool fill_button_panel ( int number_buttons,
Panel button_panel,
List < Button > buttons,
EventHandler e )
{
int available_space = 0;
Point location = new Point ( );
int right_most = 0;
int spacing = 0;
button_panel.Controls.Clear ( );
foreach ( Button button in buttons )
{
button.Click -= e;
}
buttons.Clear ( );
location.Y = 1;
available_space = button_panel.Width -
( number_buttons * BUTTON_WIDTH );
spacing = ( int ) ( ( float ) available_space /
( float ) ( number_buttons - 1 ) );
for ( int i = 0; ( i < number_buttons ); i++ )
{
Button button = new Button ( );
location.X = ( i * spacing ) + ( i * BUTTON_WIDTH );
button.BackColor = SystemColors.Control;
button.Location = location;
button.Size = BUTTON_SIZE;
button.Tag = i;
button.Click += e;
right_most = button.Location.X + button.Size.Width;
button.Visible = true;
buttons.Add ( button );
button_panel.Controls.Add ( button );
}
if ( right_most < ( button_panel.Size.Width - EPSILON ) )
{
int pixels = 1;
int start = 0;
start = buttons.Count -
( button_panel.Size.Width - right_most );
for ( int i = start;
( i < buttons.Count );
i++ )
{
location = buttons [ i ].Location;
location.X += pixels++;
buttons [ i ].Location = location;
}
}
button_panel.Visible = true;
return ( true );
}
Increasing the number of start/end buttons from two to up to six required that gradients be generated in panels and then that the panels be concatenated to form a single gradient panel.
The panels, used by both the Interpolation and the LinearGradient modes, are created by create_panels. (Note that class constants and variables that have been presented earlier are not repeated.)
List < int > panels_end = new List < int > ( );
List < int > panels_start = new List < int > ( );
int panel_width = 0;
bool create_panels ( ref List < Panel > panels )
{
Size size = new Size ( 0, BUTTON_HEIGHT );
panels.Clear ( );
panels_end.Clear ( );
panels_start.Clear ( );
size.Width = start_end_color_buttons [ 1 ].Location.X -
start_end_color_buttons [ 0 ].Location.X;
panel_width = size.Width;
for ( int i = 0;
( i < ( start_end_color_buttons.Count - 1 ) );
i++ )
{
Point location = new Point ( 0, 0 );
Panel panel = new Panel ( );
location.X =
start_end_color_buttons [ i ].Location.X;
panel.Location = location;
panel.Size = size;
panels_start.Add ( panel.Location.X );
panels_end.Add ( panel.Location.X +
panel.Size.Width );
panel.BackColor =
start_end_color_buttons [ i ].BackColor;
panel.ForeColor =
start_end_color_buttons [ i + 1 ].BackColor;
panel.Tag = i;
panels.Add ( panel );
}
return ( true );
}
The target of create_panels is a List of Panels. All the panels have the same width and there are as many panels as there are ( start/end - 1 ) buttons.
From the properties of start_end_color_buttons, collected earlier, the sizes of all of the panels are computed. When a panel has been defined, it is added to the target List of Panels. For future reference, the gradient starting and ending colors of each panel are recorded in the Backcolor and ForeColor properties, respectively, of each panel.
Coloring the Gradient Panel
Up to this point, both the Interpolation and LinearGradient algorithms are the same. Both use panels to define the geometry of the gradient_PAN. It is in the response of the display_gradient_BUT_Click event handler that divergence occurs. The display_gradient_BUT_Click event handler invokes create_gradient_PAN. This method is aware of the Interpolation and the LinearGradient modes.
bool create_gradient_PAN ( )
{
List < Panel > panels = new List < Panel > ( );
gradient_PAN.Visible = false;
gradient_PAN.Controls.Clear ( );
create_panels ( ref panels );
foreach ( Panel panel in panels )
{
if ( use_interpolation )
{
panel.Paint +=
new PaintEventHandler (
interpolated_gradient_panel_OnPaint );
}
else
{
panel.Paint +=
new PaintEventHandler (
linear_gradient_OnPaint );
}
panel.Invalidate ( );
gradient_PAN.Controls.Add ( panel );
}
gradient_PAN.Visible = true;
return ( true );
}
The create_gradient_PAN method first hides gradient_PAN and clears it of any existing content. Next it invokes create_panels to obtain the List of Panels that will be colored to form the gradient. Iterating through the List of Panels, returned by create_panels, the method declares different PaintEventHandlers[^] for the Interpolation and LinearGradient modes. It then raises the panel's Paint event by invoking Invalidate [^]. When the panel is painted, create_gradient_PAN appends the panel to the gradient_PAN creating a container that will hold the desired blend of colors.
Applying Interpolated Color
interpolated_gradient_panel_OnPaint is the handler for coloring the gradient panel in Interpolation mode. Within this method, a list of colors (colors) is created that contains a list of the colors within the panel. When the panel has been colored, colors is appended to panel_colors to allow a rapid access to the colors later during the coloring of the selector buttons. panel_colors is only used in Interpolation mode.
List < List < Color > > panel_colors =
new List < List < Color > > ( );
void interpolated_gradient_panel_OnPaint (
object sender,
PaintEventArgs e )
{
Color end_color;
List < Color > colors = new List < Color > ( );
int height = 0;
Panel panel = ( Panel ) sender;
int position = 0;
Color start_color;
base.OnPaint ( e );
end_color = panel.ForeColor;
height = panel.Size.Height;
start_color = panel.BackColor;
foreach ( Color color in interpolated_color_values (
start_color,
end_color,
panel.Size.Width ) )
{
e.Graphics.DrawLine ( new Pen ( color, 1.0F ),
new Point ( position, 0),
new Point ( position, height) );
colors.Add ( color );
position++;
}
panel_colors.Add ( colors );
}
Interpolation, in itself, required a rethinking of the algorithm that generated and placed the colors in a panel. The final algorithm draws a one pixel wide line for each interpreted color that is to appear in the panel.
In the following example, the line is drawn five pixels wide with a one pixel wide separator.
Of course, in the application, the one pixel separator is absent, allowing the lines to form a smooth, continuous gradient.
interpolated_color_values is an Iterator Method [^] suggested by Bill Woodruff.
IEnumerable < Color > interpolated_color_values (
Color start_color,
Color end_color,
int steps )
{
double start_color_red = ( double ) start_color.R;
double end_color_red = ( double ) end_color.R;
double red_difference =
( start_color.R - end_color_red ) /
( double ) steps;
double start_color_green = ( double ) start_color.G;
double end_color_green = ( double ) end_color.G;
double green_difference =
( start_color.G - end_color_green ) /
( double ) steps;
double start_color_blue = ( double ) start_color.B;
double end_color_blue = ( double ) end_color.B;
double blue_difference =
( start_color.B - end_color_blue ) /
( double ) steps;
yield return ( start_color );
for ( int i = 1; ( i < ( steps - 1 ) ); i++ )
{
yield return (
Color.FromArgb (
( int ) ( start_color_red + 0.5 ),
( int ) ( start_color_green + 0.5 ),
( int ) ( start_color_blue + 0.5 ) ) );
start_color_red =
( start_color_red - red_difference );
start_color_green =
( start_color_green - green_difference );
start_color_blue =
( start_color_blue - blue_difference );
}
yield return ( end_color );
}
In interpolated_color_values all variables are of type double with the exceptions of the loop index and the returned Color. Rounding operations are round up [^].
Applying LinearGradient Color
We are using individual panels to paint the gradient using the LinearGradientBrush. Therefore, painting the panels is simply a matter of applying a new brush to each of the panels. This is accomplished by linear_gradient_panel_OnPaint invoked by create_gradient_PAN.
void linear_gradient_panel_OnPaint ( object sender,
PaintEventArgs e )
{
Panel panel = ( Panel ) sender;
base.OnPaint ( e );
e.Graphics.FillRectangle ( new LinearGradientBrush (
panel.ClientRectangle,
panel.BackColor,
panel.ForeColor,
0.0F ),
panel.ClientRectangle );
}
Coloring the Selector Buttons
The selector buttons are placed into the selector_buttons_PAN. The user specifies how many buttons will exist and clicks the display_selector_buttons_BUT control. The handler for the click of the display_selector_buttons_BUT is display_selector_buttons_BUT_Click. The first task for the handler is to create the buttons that will be placed into the selector_buttons_PAN. This is accomplished by invoking fill_button_panel, the same method that created the start_end_buttons_PAN.
void display_selector_buttons_BUT_Click ( object sender,
EventArgs e )
{
fill_button_panel ( number_selector_buttons,
selector_buttons_PAN,
selector_buttons,
selector_button_Click );
if ( use_interpolation )
{
color_using_interpolation ( );
}
else
{
color_using_linear_gradient ( );
}
selector_buttons_PAN.Visible = true;
ascending_PB.Visible = true;
copy_left_to_right_BUT.Visible = true;
copy_format_GB.Visible = true;
rgb_decimal_RB.Visible = true;
rgb_hexadecimal_RB.Visible = true;
descending_PB.Visible = true;
copy_right_to_left_BUT.Visible = true;
reset_BUT.Visible = true;
}
Once the selector buttons have been created, the appropriate coloring algorithm is applied. Lastly, miscellaneous controls are made visible.
Applying Interpolated Color
During the execution of interpolated_gradient_panel_OnPaint, the structure panel_colors was filled with the color of each pixel within each panel. color_using_interpolation determines to which panel a selector_button is subordinate. Then, using where within the panel the center of a selector_button is located, the color of the selector_button Background can be retrieved from panel_colors. The first and last selector_buttons take the background color of the first and last start/end buttons, respectively.
bool color_using_interpolation ( )
{
for ( int i = 0; ( i < selector_buttons.Count ); i++ )
{
Button button = selector_buttons [ i ];
int button_centerline = button.Location.X +
BUTTON_WIDTH_DIV_2;
Color color = Color.Empty;
int color_at = 0;
float f0 = 0.0F;
float f1 = 0.0F;
float f2 = 0.0F;
int panel_index = 0;
if ( i == 0 )
{
button.BackColor =
start_end_color_buttons [ i ].BackColor;
continue;
}
if ( i == ( selector_buttons.Count - 1 ) )
{
button.BackColor =
start_end_color_buttons [
( start_end_color_buttons.Count - 1 ) ].
BackColor;
continue;
}
for ( int j = 0; ( j < panels_end.Count ); j++ )
{
if ( ( button_centerline >=
panels_start [ j ] ) &&
( button_centerline <=
panels_end [ j ] ) )
{
panel_index = j;
break;
}
}
f0 = ( float ) button_centerline /
( float ) panel_width;
f1 = f0 - (int ) f0;
f2 = f1 * ( float ) panel_width;
color_at = ( int ) ( f2 + 0.5F );
color = panel_colors [ panel_index ] [ color_at ];
button.BackColor = color;
}
return ( true );
}
Applying LinearGradient Color
The selector_button color is determined by the pixel color of the pixel in the gradient panel directly above the centerline of the selector_button.
As with Interpolation mode, the first and last selector_buttons take the background color of the first and last start/end buttons, respectively.
bool color_using_linear_gradient ( )
{
for ( int i = 0; ( i < selector_buttons.Count ); i++ )
{
Button button = selector_buttons [ i ];
if ( i == 0 )
{
button.BackColor =
start_end_color_buttons [ 0 ].BackColor;
}
else if ( i == ( selector_buttons.Count - 1 ) )
{
button.BackColor =
start_end_color_buttons [
( start_end_color_buttons.Count - 1 ) ].
BackColor;
}
else
{
retrieve_linear_gradient_color ( ref button );
}
button.UseVisualStyleBackColor = false;
}
return ( true );
}
The balance of the selection_buttons are processed by retrieve_linear_gradient_color.
bool retrieve_linear_gradient_color ( ref Button button )
{
Point point;
Point screen_point;
point = new Point (
( ( selector_buttons_PAN.Location.X +
button.Location.X ) +
( button.Size.Width / 2 ) ),
( gradient_PAN.Location.Y +
( gradient_PAN.Size.Height / 2 ) ) );
screen_point = PointToScreen ( point );
button.BackColor =
Win32API.get_pixel_color_at_location ( screen_point );
return ( true );
}
The Win32API.get_pixel_color_at_location method invokes the GetPixel [^] method to obtain the GDI+ color of the pixel at the specified screen position on the monitor (note this is a screen position not a client position).
The method uses GetPixel against a one by one pixel Bitmap. The pixel located at the specified location is copied using Win32 BitBlt [^] into a one-by-one Bitmap. This results in four benefits: the method will not raise an exception when used in a multi-monitor environment; it is faster than GetPixel; the Bitmap used is only one pixel high and wide; and the Bitmap is local to this method.
Acknowledgments
The authors of the readers' comments that caused this revision to be made are:
- steve-redTrans
- BillWoodruff
I thank both for their comments.
References
Conclusion
This article has reported a revision to a tool that provides its users with the ability to pick colors from a linear color gradient using from two to up to six bounding colors.
Development Environment
The Gradient Color Picker was developed in the following environment:
Microsoft Windows 7 Professional SP 1 |
Microsoft Visual Studio 2008 Professional SP1 |
Microsoft Visual C# 2008 |
Microsoft .Net Framework Version 3.5 SP1 |
History
06/10/2021 | Original article |