Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / multimedia / GDI+

Gradient Color Picker - Revised Again

5.00/5 (7 votes)
14 Jun 2021CPOL6 min read 12K   313  
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.
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.

Introduction Table of Contents

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.

gradient_color_picker

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 Table of Contents

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 Table of Contents

On initial execution, GradientColorPickerV3 displays the following:

initial gradient color picker

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.)

C#
// ******************************************* class constants

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;

// ******************************************* class variables

int                     number_start_end_buttons =
                            DEFAULT_NUMBER_START_END_BUTTONS;
List < Button >         start_end_color_buttons =
                            new List < Button > ( );

// ****************************************** show_initial_GUI

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 );

    } // show_initial_GUI

// ***************************************** fill_button_panel

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 is expected to be
                                // greater than zero
        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 );

    } // fill_button_panel

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.

gradient panels

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.)

C#
// ******************************************* class variables

List < int >            panels_end = new List < int > ( );
List < int >            panels_start = new List < int > ( );
int                     panel_width = 0;

// ********************************************* create_panels

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 );

                                // BackColor = leftmost color
        panel.BackColor =
            start_end_color_buttons [ i ].BackColor;
                                // ForeColor = rightmost color
        panel.ForeColor =
            start_end_color_buttons [ i + 1 ].BackColor;

        panel.Tag = i;

        panels.Add ( panel );
        }

    return ( true );

    } // create_panels

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 Table of Contents

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.

C#
// *************************************** create_gradient_PAN

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 );

    } // create_gradient_PAN

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 Table of Contents

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.

C#
// ******************************************* class variables

List < List < Color > > panel_colors =
                            new List < List < Color > > ( );

// *********************** interpolated_gradient_panel_OnPaint

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 );

    } // interpolated_gradient_panel_OnPaint

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.

pixel gradient colors

Of course, in the application, the one pixel separator is absent, allowing the lines to form a smooth, continuous gradient.

smoothpedixel gradient colors

interpolated_color_values is an Iterator Method [^] suggested by Bill Woodruff.

C#
// ********************************* interpolated_color_values

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 );

    } // interpolated_color_values

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 Table of Contents

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.

C#
// ***************************** linear_gradient_panel_OnPaint

void linear_gradient_panel_OnPaint ( object         sender,
                                     PaintEventArgs e )
    {
    Panel   panel = ( Panel ) sender;

    base.OnPaint ( e );
                                // BackColor => start
                                // ForeColor => end
    e.Graphics.FillRectangle ( new LinearGradientBrush (
                                       panel.ClientRectangle,
                                       panel.BackColor,
                                       panel.ForeColor,
                                       0.0F ),
                               panel.ClientRectangle );

    } // linear_gradient_panel_OnPaint

Coloring the Selector Buttons Table of Contents

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.

C#
// ************************ display_selector_buttons_BUT_Click

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;

    } // display_selector_buttons_BUT_Click

Once the selector buttons have been created, the appropriate coloring algorithm is applied. Lastly, miscellaneous controls are made visible.

Applying Interpolated Color Table of Contents

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.

pixel gradient with selectors

C#
// ********************************* color_using_interpolation

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 );

    } // color_using_interpolation

Applying LinearGradient Color Table of Contents

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.

gradient with selectors

As with Interpolation mode, the first and last selector_buttons take the background color of the first and last start/end buttons, respectively.

C#
// ******************************* color_using_linear_gradient

bool color_using_linear_gradient ( )
    {

    for ( int i = 0; ( i < selector_buttons.Count ); i++ )
        {
        Button  button = selector_buttons [ i ];
                                // color the button based upon
                                // its current location in the
                                // buttons panel
        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 );

    } // color_using_linear_gradient

The balance of the selection_buttons are processed by retrieve_linear_gradient_color.

C#
// **************************** 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 );

    } // retrieve_linear_gradient_color

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 Table of Contents

The authors of the readers' comments that caused this revision to be made are:

  • steve-redTrans
  • BillWoodruff

I thank both for their comments.

References Table of Contents

Conclusion Table of Contents

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 Table of Contents

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 Table of Contents

06/10/2021 Original article

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)