渐变颜色选择器——再次修订





5.00/5 (7投票s)
本文修订了早期版本的渐变色拾取器(V2)。修订的动力来自读者要求增加初始颜色选择的数量。
目录
引言
本文修订了早期版本的 渐变色拾取器 (V2) [^]。修订的动力来自读者要求增加初始颜色选择的数量。
实现了两项更改
- 增加了起始/结束按钮的数量。
- 提供了插值作为 Microsoft LinearGradientBrush [^] 的替代方案。由此产生的两种算法分别命名为线性渐变和插值。
实现
在可能的情况下,对实现所需更改的修改旨在最大程度地减少对现有软件的影响。然而,在此过程中检测并消除了许多低效率。
增加了起始/结束按钮的数量
首次运行时,GradientColorPickerV3 显示如下
除其他外,它会调用show_initial_GUI 来创建初始用户界面 (UI)。用户通过在起始/结束按钮的 NumericUpDown (NUD) 控件中提供所需数量来指定按钮的数量。number_start_end_buttons 记录 NUD 的值。每次 NUD 值更改时,显示都会更新以显示新数量的未着色按钮。
show_initial_GUI 调用 fill_button_panel。
当两个按钮面板(start_end_buttons_PAN 或 selector_buttons_PAN)中的任何一个需要重绘时,都会调用 fill_button_panel。(请注意,类常量和变量已完整呈现。)
// ******************************************* 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
将起始/结束按钮的数量从两个增加到最多六个,需要先在面板中生成渐变,然后将这些面板连接起来形成一个单一的渐变面板。
插值和线性渐变模式都使用的面板由 create_panels 创建。(请注意,前面已呈现的类常量和变量不再重复。)
// ******************************************* 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
create_panels 的目标是一个 Panel 控件的 List。所有面板的宽度相同,面板数量等于(起始/结束 - 1)按钮的数量。
根据之前收集的 start_end_color_buttons 的属性,计算所有面板的大小。定义一个面板后,将其添加到目标 Panel 控件的 List 中。为将来参考,每个面板的渐变起始颜色和结束颜色分别记录在每个面板的Backcolor和ForeColor属性中。
渐变面板的着色
到目前为止,插值和线性渐变算法是相同的。两者都使用面板来定义gradient_PAN 的几何形状。分歧发生在display_gradient_BUT_Click 事件处理程序的响应中。display_gradient_BUT_Click 事件处理程序调用 create_gradient_PAN。此方法了解插值和线性渐变模式。
// *************************************** 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
create_gradient_PAN 方法首先隐藏 gradient_PAN 并清除其现有内容。接下来,它调用 create_panels 来获取将着色以形成渐变的 Panel 控件的 List。通过迭代 create_panels 返回的 Panel 控件的 List,该方法为插值和线性渐变模式声明不同的 PaintEventHandler [^]。然后,通过调用 Invalidate [^],它会触发面板的 Paint 事件。当面板绘制完成后,create_gradient_PAN 将该面板附加到 gradient_PAN,创建一个将包含所需颜色混合的容器。
应用插值颜色
interpolated_gradient_panel_OnPaint 是在插值模式下为渐变面板着色的处理程序。在此方法中,将创建一个颜色列表(colors),其中包含面板内颜色的列表。当面板着色后,colors 将被附加到 panel_colors,以便在稍后着色选择器按钮时能够快速访问颜色。panel_colors 仅在插值模式下使用。
// ******************************************* 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
插值本身需要重新思考生成颜色并将其放置在面板中的算法。最终算法为面板中要出现的每种解释颜色绘制一条一像素宽的线。
在下面的示例中,线条的宽度为五像素,中间有一个一像素的间隔。
当然,在应用程序中,一像素的间隔是不存在的,允许线条形成平滑、连续的渐变。
interpolated_color_values 是 Bill Woodruff 建议的一个 迭代器方法 [^]。
// ********************************* 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
在 interpolated_color_values 中,除循环索引和返回的 Color 外,所有变量的类型都是 double。舍入操作是向上舍入 [^]。
应用线性渐变颜色
我们使用独立的面板通过LinearGradientBrush 来绘制渐变。因此,绘制面板只需为每个面板应用一个新的画笔。这是通过 linear_gradient_panel_OnPaint 实现的,它由 create_gradient_PAN 调用。
// ***************************** 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
选择器按钮的着色
选择器按钮被放置在selector_buttons_PAN 中。用户指定按钮的数量并单击display_selector_buttons_BUT 控件。display_selector_buttons_BUT 点击事件的处理程序是 display_selector_buttons_BUT_Click。处理程序的第一项任务是创建将放置在selector_buttons_PAN 中的按钮。这通过调用fill_button_panel 来实现,该方法与创建start_end_buttons_PAN 的方法相同。
// ************************ 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
一旦创建了选择器按钮,就会应用相应的着色算法。最后,使各种控件可见。
应用插值颜色
在执行 interpolated_gradient_panel_OnPaint 期间,结构体 panel_colors 被填充了每个面板内每个像素的颜色。color_using_interpolation 确定一个selector_button 属于哪个面板。然后,利用selector_button 中心在面板中的位置,可以从panel_colors 中检索selector_button 背景的颜色。第一个和最后一个selector_button 分别采用第一个和最后一个起始/结束按钮的背景颜色。
// ********************************* 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
应用线性渐变颜色
selector_button 的颜色由selector_button 中心线正上方的渐变面板像素的颜色决定。
与插值模式一样,第一个和最后一个selector_button 分别采用第一个和最后一个起始/结束按钮的背景颜色。
// ******************************* 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
其余的selection_buttons 由 retrieve_linear_gradient_color 处理。
// **************************** 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
Win32API.get_pixel_color_at_location 方法调用 GetPixel [^] 方法以获取指定屏幕位置(注意这是屏幕位置而不是客户端位置)的 GDI+ 像素颜色。
该方法使用 GetPixel 处理一个像素大小的 Bitmap。通过 Win32 BitBlt [^] 将指定位置的像素复制到一个像素大小的 Bitmap 中。这带来四个好处:该方法在多显示器环境中运行时不会引发异常;它比 GetPixel 快;使用的 Bitmap 仅为一像素高和一像素宽;并且该 Bitmap 仅在此方法本地。
致谢
导致本次修订的读者评论的作者是
- steve-redTrans
- BillWoodruff
我感谢他们两位的评论。
参考
- BitBlt [^]
- GetPixel [^]
- 渐变色拾取器 (V2) [^]
- Invalidate [^]
- 迭代器方法 [^]
- LinearGradientBrush [^]
- PaintEventHandler [^]
- 向上舍入 [^]
结论
本文报告了一个工具的修订,该工具使用户能够从线性颜色渐变中拾取颜色,该渐变具有从两个到最多六个边界颜色。
开发环境
渐变色拾取器是在以下环境中开发的
Microsoft Windows 7 专业版 SP 1 |
Microsoft Visual Studio 2008 专业版 SP1 |
Microsoft Visual C# 2008 |
Microsoft .Net Framework Version 3.5 SP1 |
历史
06/10/2021 | 原文 |