Title: Graphically select hours in C#
The DateTimePicker control lets you select dates and date ranges, but there's no standard control that lets you select hours. This example shows one way to do this with a PictureBox.
A big part of this program is the DrawHours method shown in the following code. It draws the selected hours.
// The selected hours.
private int StartHour = 6;
private int StopHour = 18;
// Draw the hour indicator on this PictureBox.
private void DrawHours(PictureBox pic, Graphics gr,
int start_hour, int stop_hour)
{
gr.Clear(Color.LightGreen);
// Scale to fit a 24-hour period.
const int margin = 3;
float scale_x = XScale(pic);
float hgt = pic.ClientSize.Height;
float y1 = margin;
float y2 = hgt - margin;
// Draw the selected time range.
RectangleF hours_rect =
new RectangleF(
start_hour * scale_x, y1,
(stop_hour - start_hour) * scale_x, y2 - y1);
gr.FillRectangle(Brushes.Blue, hours_rect);
// Draw tick marks.
float x = 0;
for (int i = 0; i <= 24; i++)
{
gr.DrawLine(thin_pen, x, 0, x, hgt);
x += scale_x;
}
// Draw times.
gr.RotateTransform(-90);
int xmid = -pic.ClientSize.Height / 2;
using (Font font = new Font(FontFamily.GenericSansSerif, 12))
{
using (StringFormat sf = new StringFormat())
{
sf.Alignment = StringAlignment.Center;
sf.LineAlignment = StringAlignment.Center;
x = 0;
for (int i = 0; i <= 24; i++)
{
gr.DrawString(HourToString(i), font,
Brushes.Black, xmid, x, sf);
x += scale_x;
}
}
}
}
This method clears the PictureBox with light green. It then calculates a horizontal scale factor scale_x to use when drawing and uses it to fill a rectangle representing the selected hours in blue. Next the code loops over 25 hours (0 through 24) adding tick marks to show where the hours are in the drawing.
The method then adds a -90 degree rotation (90 degrees counter-clockwise) to the Graphics object to rotate future drawing. It finishes by looping through the hours again to draw each hour's name. It uses the following HourToString method to convert an hour number into its name.
// Return the hour formatted as we want to display it.
private string HourToString(int hour)
{
if (hour == 0) return "midnight";
if (hour == 12) return "noon";
if (hour == 24) return "midnight";
if (hour <= 12) return hour.ToString() + "am";
return (hour - 12).ToString() + "pm";
}
The HourToString method simply returns a string appropriate for an hour number.
The following code shows the XScale method that the program uses to calculate a horizontal scale factor.
// Get the horizontal scale factor.
private float XScale(PictureBox pic)
{
return pic.ClientSize.Width / 24;
}
The scale factor is just the width of the PictureBox control's client area divided by the number of hours the program draws.
The rest of the interesting code handles the mouse events that let the user select hours. The following MouseDown event handler starts the process.
// Handle mouse events.
private bool Drawing = false;
private int DrawingStartHour, DrawingStopHour;
private void picHours_MouseDown(object sender, MouseEventArgs e)
{
Drawing = true;
DrawingStartHour = (int)Math.Round(e.X / XScale(picHours));
DrawingStopHour = DrawingStartHour;
StartHour = DrawingStartHour;
StopHour = DrawingStartHour;
picHours.Refresh();
}
The event handler sets Drawing = true to indicate that the user has pressed the mouse. It divides the mouse's X position by the horizontal scale factor to see what hour is being selected, rounding to the nearest hour. The event handler saves the hour in the variables DrawingStartHour, DrawingStopHour, StartHour, and StopHour, and refreshes the PictureBox to make it redraw.
The following code shows the MouseMove event handler.
private void picHours_MouseMove(object sender, MouseEventArgs e)
{
if (!Drawing) return;
// Calculate the value and display a tooltip.
int hour = (int)Math.Round(e.X / XScale(picHours));
string new_tip = HourToString(hour);
if (tipHour.GetToolTip(picHours) != new_tip)
tipHour.SetToolTip(picHours, new_tip);
// Save the new value.
DrawingStopHour = hour;
// Redraw.
if (DrawingStartHour < DrawingStopHour)
{
StopHour = DrawingStopHour;
StartHour = DrawingStartHour;
}
else
{
StartHour = DrawingStopHour;
StopHour = DrawingStartHour;
}
picHours.Refresh();
}
This code gets the hour under the mouse the same way the MouseDown event handler does. It calls HourToString to get the hour's name and compares it to the PictureBox control's current tooltip. If the name is different from the current tooltip, the code sets the tooltip to the new hour name. (You could remove this if the tooltip annoys you. I'm on the fence on this one.)
Next, the code saves the hour in DrawingStopHour. It then sets StartHour and StopHour so StartHour <= StopHour. (If you don't do this, the DrawHours method earlier doesn't correctly draw the selected hours because it tries to draw a RectangleF with a negative width and the Graphics object won't do that.)
Finally, the method refreshes the PictureBox to make it redraw.
The last interesting piece of code is the following MouseUp event handler.
private void picHours_MouseUp(object sender, MouseEventArgs e)
{
if (!Drawing) return;
tipHour.SetToolTip(picHours, "");
Drawing = false;
DisplayTimes();
}
This event handler simply clears the PictureBox control's tooltip and calls the following DisplayTimes method to display the selected time range in textboxes.
// Show the times in the TextBoxes.
private void DisplayTimes()
{
DateTime start_time = new DateTime(2000, 1, 1, StartHour, 0, 0);
DateTime stop_time = new DateTime(2000, 1, 1, StopHour, 0, 0);
txtStartTime.Text = start_time.ToShortTimeString();
txtStopTime.Text = stop_time.ToShortTimeString();
}
This method converts the selected times into DateTime values and then uses their ToShortTimeString method to produce a nicely formatted output.
There are lots of interesting modifications you could make to this code. For example, you could allow the user to select times in half hour or quarter hour increments. Or you could draw different selected hours in different colors to show overtime or normal business hours. (I may add some of those in a later example.)
Download the example to experiment with it and to see additional details.
|