Table of Contents
The symbol
returns the reader to the top of the Table of Contents.
1. Introduction
This article is the last planned revision to earlier versions of the ViewFile tool. What triggered the revision was the large size of a file I was creating and the need to examine a specific byte within that file. Scrolling down through the file until I reached the desired byte did not seem like the best user experience.
The revision includes:
- Addition of a Start At TextBox that allows a user to specify the byte location where the display is to start.
- Setting the background color of read only objects to the GradientActiveCaption system color.
- Increasing the font of the user interface.
- Removal of the Head application
2. Background
Often, during the development process, it is useful to know the contents of a file in a format that is not the default format provided by applications. For example, a .txt file is usually displayed by Notepad as a series of lines of text; a .png file by an image viewer as a picture. Normally, default Windows tools do not offer the option to display the contents of a file in a byte-ordered display. ViewFile seeks to repair that ommission.
3. A Note on Typography and Images
In the following discussions, objects, used internally by the software are displayed in italicized text; objects that are accessed by the user are displayed in italicized bold text. For those images whose contents are difficult to read, the image is a thumbnail. Upon clicking on the image, a larger version will be displayed using the default image viewer.
4. ViewFile

ViewFile is a straight-forward file dump facility. On its initial execution, ViewFile accepts the name of an input file when the user clicks on the Browse button.
ViewFile provides a last directory visited feature that "remembers" the path of the last file examined. This path is placed into a file, named last_directory.dat, for later reference. Because last_directory.dat may be revised during the execution of ViewFile, it is placed into the user's profile folder. The path in Windows 7 is C:\Users\<Username>\ViewFile\last_directory.dat. If that file does not exist, it will be created and the last directory visited will be set to C:\Users\<Username>\ViewFile and saved in the newly created file.
ViewFile is reentrant, meaning that the Browse button may be clicked again to supply a new input file name.
When the user has provided the name of the input file, ViewFile saves that file's directory path in last_directory.dat. Viewfile then displays the file's size, a TextBox into which the user may specify the byte at which the display is to start, and a Go button that starts the display.
When the user clicks the Go button, the file contents are displayed, starting at the value in the Start At TextBox.
Note that, in this case, the file whose contents are being displayed is a Portable Network Graphics file (recognized not only by its name, .png, but also by its file signature, 89 50 4E 47 0D 0A 1A 0A). For a good source of file signatures see Gary Kessler's File Signatures Table [^].
The user may scroll up and down through the file contents using the vertical scroll bar to the right of the window.
ViewFile also responds to the following keyboard keys.
- Home
- Page Up
- End
- Page Down
- Up Arrow
- Down Arrow
ViewFile responds to a key that is being held down (with the exception of Home and End).
The user may choose to:
- Change the left-hand format from hexadecimal to decimal.
- Cause the display to stay on top of all other desktop windows.
- Replace spaces on the right-hand side by the characters "<SP>". This is useful when the number of space characters on the right may not be immediately apparent.
5. Implementation
The ViewFile constructor is:
:
:
const int TIMER_REPEAT_DELAY = 400;
:
public ViewFile ( )
{
InitializeComponent ( );
Application.ApplicationExit += new EventHandler (
OnApplicationExit );
OnApplicationExit ( this, EventArgs.Empty );
if ( SystemInformation.MouseWheelPresent )
{
contents_TB.MouseWheel += new MouseEventHandler (
TB_MouseWheel );
}
timer = new Timer ( );
timer.Interval = TIMER_REPEAT_DELAY;
timer.Tick += new EventHandler ( TIMER_Tick );
timer.Enabled = false;
initialize_global_variables ( contents_TB );
signon ( );
initialize_GUI ( contents_TB );
}
contents_TB is the TextBox that displays the contents of the user-chosen file. It is declared in the Visual Studio Designer [^]. TIMER_REPEAT_DELAY is a global constant that contains the number of milliseconds (400) before the timer Tick event is raised again. The value cannot be less than one. The value of 400 milliseconds appears to be a reasonable choice for the timer repeat interval.
After these tasks complete, ViewFile is wholly event driven.
5.1. ViewFile Event Handlers
5.1.1. OnApplicationExit Event Handler
Although this event handler is not strictly required, it is declared so that the FileStream [^], whose contents are to be displayed, will be closed and disposed and the Windows Timer [^] will be stopped and disposed when the application exits.
using System;
:
using System.IO;
:
using System.Windows.Forms;
:
:
FileStream file_stream = null;
:
Timer timer = null;
:
void OnApplicationExit ( object sender,
EventArgs e )
{
if ( file_stream != null )
{
file_stream.Close ( );
file_stream.Dispose ( );
file_stream = null;
}
if ( timer != null )
{
if ( timer.Enabled )
{
timer.Stop ( );
}
timer.Dispose ( );
timer = null;
}
}
5.1.2. BUT_Click Event Handler
The BUT_Click event handler fields the Button [^] Click Events [^] of the Browse, Go, and Exit buttons.
void BUT_Click ( object sender,
EventArgs e )
{
Button button = ( Button ) sender;
string name = button.Name.Trim ( );
switch ( name )
{
case "browse_BUT":
if ( browse ( ) )
{
if ( open_file_stream ( ) )
{
if ( get_file_statistics ( ) )
{
file_size_TB.Text =
file_bytes.ToString ( );
start_at = 0;
start_at_TB.Text =
start_at.ToString ( );
start_at_and_go_GB.Visible = true;
}
}
}
break;
case "go_BUT":
if ( start_at_valid ( ) )
{
display_file ( );
contents_TB.Focus ( );
}
break;
case "exit_BUT":
Application.Exit ( );
break;
default:
throw new ApplicationException (
String.Format (
"{0} is not a recognized Button name",
name ) );
}
}
browse obtains the filename of the file that is to be displayed using the last_directory; open_file_stream attempts to open the user specified file; and get_file_statistics obtains the length of the input file in bytes.
start_at_valid determines if the user supplied value is valid. If there were no errors, display_file is invoked. Note that display_file is invoked only once for each file.
:
:
VScrollBar textbox_VSB = null;
:
void display_file ( )
{
new_value = start_at / MAXIMUM_ENTRIES_PER_LINE;
textbox_VSB.Minimum = 0;
textbox_VSB.Maximum = file_lines;
textbox_VSB.Value = 0;
display_changed = true;
options_and_size_GB.Visible = true;
display_input_file ( );
}
When the file specific variables have been initialized, display_file invokes display_input_file.
display_changed must be set true before display_input_file is invoked. In this case, both new_value and textbox_VSB.Value are set to zero. Normally, in display_input_file, this situation would be interpreted as "no need to redraw." However, display_file is invoked when a newly opened file is to be displayed. Therefore it is necessary to force display_input_file to redraw the display. Other instances where display_changed must be set to true are when the options Hexadecimal or Use <SP> for Spaces change the display itself. In those cases no change to new_value or textbox_VSB.Value occurs. Unless display_changed is set true, the display will not reflect the user's wishes.
display_input_file is the workhorse of ViewFile.
:
const int MAXIMUM_BUFFER = MAXIMUM_LINES *
MAXIMUM_ENTRIES_PER_LINE;
const int MAXIMUM_ENTRIES_PER_LINE = 8;
const int MAXIMUM_LINES = 20;
:
:
FileStream file_stream = null;
:
int new_value = 0;
VScrollBar textbox_VSB = null;
:
void display_input_file ( )
{
byte [ ] buffer = new byte [ MAXIMUM_BUFFER ];
int buffer_index = 0;
int byte_offset = 0;
int bytes_read = 0;
StringBuilder ch_buffer = new StringBuilder ( );
StringBuilder digits_buffer = new StringBuilder ( );
int lines_read = 0;
int remainder = 0;
int starting_byte = 0;
new_value = Math.Max (
0,
Math.Min ( new_value,
( textbox_VSB.Maximum -
MAXIMUM_LINES ) ) );
if ( display_changed )
{
display_changed = false;
}
else if ( new_value == textbox_VSB.Value )
{
return;
}
contents_TB.Suspend ( );
contents_TB.Clear ( );
byte_offset = new_value * MAXIMUM_ENTRIES_PER_LINE;
textbox_VSB.Value = new_value;
read_data ( file_stream,
byte_offset,
buffer,
ref bytes_read );
lines_read = bytes_read / MAXIMUM_ENTRIES_PER_LINE;
remainder = bytes_read % MAXIMUM_ENTRIES_PER_LINE;
ch_buffer.Length = 0;
digits_buffer.Length = 0;
buffer_index = 0;
starting_byte = byte_offset;
start_at_TB.Text = starting_byte.ToString ( );
for ( int line = 0; ( line < lines_read ); line++ )
{
for ( int j = 0;
( j < MAXIMUM_ENTRIES_PER_LINE );
j++ )
{
insert_a_byte ( ref ch_buffer,
ref digits_buffer,
buffer [ buffer_index++ ] );
}
complete_line ( ref ch_buffer,
ref digits_buffer,
ref starting_byte,
ref contents_TB );
}
if ( remainder > 0 )
{
int empty_entries = 0;
for ( int j = 0; ( j < remainder ); j++ )
{
insert_a_byte ( ref ch_buffer,
ref digits_buffer,
buffer [ buffer_index++ ] );
}
empty_entries = MAXIMUM_ENTRIES_PER_LINE -
remainder;
for ( int j = 0; ( j < empty_entries ); j++ )
{
ch_buffer.Append ( " " );
}
complete_line ( ref ch_buffer,
ref digits_buffer,
ref starting_byte,
ref contents_TB );
}
contents_TB.Select( 0, 0 );
contents_TB.ScrollToCaret ( );
contents_TB.Resume ( );
contents_TB.Visible = true;
textbox_VSB.Visible = true;
options_and_size_GB.Visible = true;
}
If the global variable display_changed is true, contents_TB will be redrawn, otherwise, the value of the global variable new_value controls the execution of display_input_file. Initially, its value is set to 0 and the value in display_changed is set to true. These values insure that display_input_file fills contents_TB with the first 160 bytes of the input file. Thereafter, new_value is modified in response to events.
Once display_input_file has retrieved the data from the file, it fills contents_TB. If the number of bytes read is not evenly divisible by the number of bytes in a contents_TB line (MAXIMUM_ENTRIES_PER_LINE). Filling contents_TB occurs in two steps: the first fills contents_TB with "whole" lines; the second fills contents_TB with the remaining bytes.
Note that the number of bytes read by display_input_file is always 160 or less (less if the end of file is encountered on filestream). Because ViewFile interacts with the user, there is no advantage to performing any kind of optimizations. Any performance gained would be offset by ViewFile's user interface.
When display_input_file completes execution, ViewFile goes idle until a keyboard key is pressed, the mouse wheel is rotated, or until the contents_TB vertical scroll bar is modified.
5.1.3. TIMER_Tick Event Handler
Prior to discussing the keyboard event handlers, I address the TIMER_Tick event handler because it is indirectly invoked by the TB_KeyDown event handler and its actions are modified by the TB_KeyUp event handler.
TIMER_Tick is relatively simple. It is invoked when timer.Enabled is set true and the timer.Interval expires.
:
:
const int TIMER_REPEAT_DELAY = 400;
:
:
bool key_down = false;
:
int lines_to_scroll = 0;
:
int new_value = 0;
:
void TIMER_Tick ( object sender,
EventArgs e )
{
timer.Interval = TIMER_REPEAT_DELAY;
if ( key_down )
{
if ( lines_to_scroll != 0 )
{
new_value = textbox_VSB.Value + lines_to_scroll;
display_input_file ( );
}
}
}
Its first action is to reset the timer.Interval to the value of TIMER_REPEAT_DELAY. I believe that 400 milliseconds are sufficient to provide a reasonable repeat rate. Two additional conditions must be met before TIMER_Tick continues its execution: key_down must be true and lines_to_scroll must be non-zero. If these conditions are met, new_value is revised and display_input_file is invoked.
5.1.4. TB_KeyDown and TB_KeyUp Event Handlers
Of these two event handlers, TB_KeyUp is by far the simplest.
void TB_KeyUp ( object sender,
KeyEventArgs e )
{
key_down = false;
timer.Enabled = false;
}
When a key is released, TB_KeyUp is invoked. Its purpose is to cancel any further repeat actions by TIMER_Tick. By setting the two conditions required for TIMER_Tick to execute (i.e., timer.Enabled and key_down) to false, TIMER_Tick stops executing. In turn, further revisions to contents_TB are halted.
Although TB_KeyUp is more complex, its logic is relatively simple. Its purpose is to determine if a keyboard key of interest has been pressed and, if so, dispatch the appropriate action.
void TB_KeyDown ( object sender,
KeyEventArgs e )
{
Keys key = e.KeyCode;
bool trigger_timer = true;
if ( key == Keys.Down )
{
lines_to_scroll = textbox_VSB.SmallChange;
}
else if ( key == Keys.Up )
{
lines_to_scroll = -textbox_VSB.SmallChange;
}
else if ( key == Keys.PageDown )
{
lines_to_scroll = textbox_VSB.LargeChange;
}
else if ( key == Keys.PageUp )
{
lines_to_scroll = -textbox_VSB.LargeChange;
}
else if ( key == Keys.Home )
{
trigger_timer = false;
lines_to_scroll = int.MinValue;
}
else if ( key == Keys.End )
{
trigger_timer = false;
lines_to_scroll = int.MaxValue;
}
else
{
lines_to_scroll = 0;
}
if ( lines_to_scroll != 0 )
{
new_value = textbox_VSB.Value + lines_to_scroll;
if ( trigger_timer )
{
timer.Interval = 1;
key_down = true;
timer.Enabled = true;
}
else
{
display_input_file ( );
}
}
}
The first task of TB_KeyDown is to determine what key was pressed and set lines_to_scroll to its appropriate value. With the exception of the Home and End keys, the values assigned to lines_to_scroll are those associated with the equivalent contents_TB's vertical scroll bar controls.
For the Home and End keys, lines_to_scroll is set to int.MinValue [^] and int.MaxValue [^] respectively. Setting lines_to_scroll to these values will insure that new_value will be assigned a value outside of the permissible range of the contents_TB vertical scroll bar.
Note that lines_to_scroll will be zero if the key pressed is not one of those in which we are interested; thus causing TB_KeyDown to return, taking no further action. Otherwise, TB_KeyDown will compute new_value. If a key other than Home or End was recognized (i.e., trigger_timer is true), the timer.Interval is set to one and both key_down and timer.Enabled are set true. Setting timer.Interval to one causes TIMER_Tick to execute almost immediately.
For the Home and End keys, once new_value is computed, display_input_file can be invoked.
5.1.5. TB_MouseWheel Event Handler
The MouseWheel Event [^] is raised when the mouse wheel moves. This event is handled by the TB_MouseWheel event handler.
void TB_MouseWheel ( object sender,
MouseEventArgs e )
{
int lines_to_move = 0;
lines_to_move =
( e.Delta * mouse_wheel_scroll_lines ) /
DELTA_UNITS_OF_WHEEL_MOVEMENT;
new_value = lines_to_move + textbox_VSB.Value;
display_input_file ( );
}
e.Delta contains a signed count of the number of detents the mouse wheel has rotated. A detent is one notch of the mouse wheel.
From MSDN:
The mouse wheel combines the features of a wheel and a mouse button. The wheel has discrete, evenly spaced notches. When you rotate the wheel, a wheel message is sent as each notch is encountered. One wheel notch, a detent, is defined by the windows constant WHEEL_DELTA [which in turn is found in SystemInformation.MouseWheelScrollLines], which is 120. A positive value indicates that the wheel was rotated forward, away from the user; a negative value indicates that the wheel was rotated backward, toward the user.
Currently, a value of 120 is the standard for one detent. If higher resolution mice are introduced, the definition of WHEEL_DATA might become smaller. Most applications should check for a positive or negative value rather than an aggregate total.
Because TB_MouseWheel is invoked each time that the mouse wheel moves, the computation of lines_to_move is straight-forward. The global constant DELTA_UNITS_OF_WHEEL_MOVEMENT is defined as 120, thus taking into effect the value of WHEEL_DELTA.
lines_to_move will be positive or negative, depending upon the direction in which the mouse wheel was moved. From that value, new_value can be computed and display_input_file invoked.
5.1.6. CHKBX_CheckedChanged Event Handler
All change events for the application's CheckBoxes are routed through the CHKBX_CheckedChanged event handler.
:
:
bool display_changed = true;
:
bool hexadecimal = true;
:
bool keep_on_top = false;
:
bool use_SP = false;
:
void CHKBX_CheckedChanged ( object sender,
EventArgs e )
{
CheckBox check_box = ( CheckBox ) sender;
bool is_checked = check_box.Checked;
string name = check_box.Name.ToString ( );
switch ( name )
{
case "hexadecimal_CHKBX":
hexadecimal = is_checked;
display_changed = true;
display_input_file ( );
break;
case "keep_on_top_CHKBX":
keep_on_top = is_checked;
this.TopMost = keep_on_top;
break;
case "use_SP_for_spaces_CHKBX":
use_SP = is_checked;
display_changed = true;
display_input_file ( );
break;
default:
throw new ApplicationException (
String.Format (
"{0} is not a recognized CheckBox name",
name ) );
}
}
CheckBoxes [^] provide the user with the ability to change the way in which ViewFile displays its data. Hexadecimal and Use <SP> for Spaces change the display itself; Keep on Top causes ViewFile to be displayed as the topmost form on the desktop. All CheckBoxes are toggles: they can be checked to activate the display option or cleared to deactivate the display option.
display_changed is set when the change in an option requires that the ViewFile display be redrawn.
5.1.7. numeric_TB_KeyPress Event Handler
When the start_at_TB TextBox was added, the requirement to insure that only numeric values were entered was also added. The numeric_TB_KeyPress event handler met the requirement.
private void numeric_TB_KeyPress ( object sender,
KeyPressEventArgs e )
{
if ( !( char.IsControl ( e.KeyChar ) ||
char.IsDigit ( e.KeyChar ) ) )
{
e.Handled = true;
}
}
When a key is pressed while the start_at_TB TextBox has focus, the handler is triggered. Setting e.Handled to true informs the operating system that the event was handled. What is accepted are the numeric digits and the control keys. By accepting control keys the contents of the start_at_TB TextBox may be edited.
5.2. Initialize application global variables
:
:
bool hexadecimal = true;
string input_file_name = String.Empty;
bool keep_on_top = false;
string last_initial_directory_filename = String.Empty;
:
bool use_SP = false;
string user_viewfile_directory = String.Empty;
:
void initialize_global_variables ( TextBox text_box )
{
determine_textbox_geometry ( text_box );
hexadecimal = true;
input_filename = String.Empty;
keep_on_top = false;
start_at = 0;
use_SP = false;
user_viewfile_directory =
( String.Format (
"{0}/{1}",
Environment.GetEnvironmentVariable (
"USERPROFILE" ),
Application.ProductName.
Replace ( " ", "_" ) ) ).
Replace ( @"\", "/" );
if ( !Directory.Exists ( user_viewfile_directory ) )
{
Directory.CreateDirectory ( user_viewfile_directory );
}
last_initial_directory_filename =
String.Format ( "{0}/last_directory.dat",
user_viewfile_directory );
initialize_tooltips ( );
}
initialize_global_variables determines the contents_TB geometry so that the maximum_textbox_lines is available to the application.Then it sets global variables to their initial values. Once set, they may be changed by the user through the user interface. initialize_global_variables also establishes the last_initial_directory_filename that will be used when the Browse button is clicked. Lastly, initialize_global_variables sets a ToolTip [^] on each user accessible control.
5.2.1. determine_textbox_geometry
:
:
int maximum_textbox_lines = 0;
:
void determine_textbox_geometry ( TextBox text_box )
{
int character_height = 0;
Font font = text_box.Font;
Size proposed_size = new Size ( int.MaxValue,
int.MaxValue );
for ( int i = SPACE; ( i <= TILDE ); i++ )
{
char ch = Convert.ToChar ( i );
Size size;
string str = ch.ToString ( );
size = TextRenderer.MeasureText (
str,
font,
proposed_size,
TextFormatFlags.Default );
if ( size.Height > character_height )
{
character_height = size.Height;
}
}
maximum_textbox_lines = text_box.Size.Height /
character_height;
}
determine_textbox_geometry determines the maximum_textbox_lines of the contents_TB TextBox. It uses TextRenderer.MeasureText [^] to obtain the largest character_height of the printable characters. character_height (in pixels) is then divided into the Height of the contents_TB TextBox. Note that this is integer division, so the quotient is truncated.
5.3. signon
void signon ( )
{
string [ ] pieces;
pieces = this.ProductVersion.Split (
new char [ ] { '.' },
StringSplitOptions.RemoveEmptyEntries );
this.Text = String.Format ( "{0} V{1}.{2}",
this.ProductName,
pieces [ 0 ],
pieces [ 1 ] );
this.Icon = Properties.Resources.ViewFileIcon;
this.StartPosition = FormStartPosition.CenterScreen;
}
The values of ProductName and ProductVersion are defined in AssemblyInfo.cs
5.4. Initialize the GUI
:
:
VScrollBar textbox_VSB = null;
:
void initialize_GUI ( TextBox text_box )
{
if ( textbox_VSB != null )
{
text_box.Controls.Remove ( textbox_VSB );
textbox_VSB.Dispose ( );
textbox_VSB = null;
}
textbox_VSB = initialize_VScrollBar ( text_box );
text_box.Controls.Add ( textbox_VSB );
textbox_VSB.Visible = false;
text_box.Visible = false;
browse_BUT.Visible = true;
input_file_LAB.Visible = true;
input_file_TB.ForeColor = Color.Gray;
input_file_TB.Text = BROWSE_PROMPT;
input_file_TB.Visible = true;
file_size_LAB.Visible = true;
file_size_TB.Visible = true;
file_bytes = 0;
file_size_TB.Text = file_bytes.ToString ( );
start_at_LAB.Visible = true;
start_at_TB.Visible = true;
start_at = 0;
start_at_TB.Text = start_at.ToString ( );
go_BUT.Visible = true;
start_at_and_go_GB.Visible = false;
hexadecimal_CHKBX.Checked = hexadecimal;
hexadecimal_CHKBX.Visible = true;
keep_on_top_CHKBX.Checked = keep_on_top;
keep_on_top_CHKBX.Visible = true;
use_SP_for_spaces_CHKBX.Checked = use_SP;
use_SP_for_spaces_CHKBX.Visible = true;
options_and_size_GB.Visible = false;
exit_BUT.Visible = true;
}
Up until now, with the exception of one, all GUI elements have been defined. This last control is the vertical scroll bar (textbox_VSB) inside the contents_TB.
5.4.1. Initialize the Vertical Scroll Bar
A Vertical ScrollBar [^] consists of a shaded shaft with an arrow button at each end and a scroll box (sometimes called a thumb) between the arrow buttons.
Minimum specifies the scrollbar value at the top of the scrollbar
Clicking the Line up arrow moves the thumb up the number of lines specified in the SmallChange property (defaults to 1)
Clicking in the Page up area moves the thumb up the number of lines specified in the LargeChange property (defaults to 10)
Thumb is the current position (at the property Value)
Clicking in the Page down area moves the thumb down the number of lines specified in the LargeChange property (defaults to 10)
Clicking the Line down arrow moves the thumb down the number of lines specified in the SmallChange property (defaults to 1)
Maximum specifies the scrollbar value at the bottom of the scrollbar
At run-time, the vertical scrollbar is placed on the right side of the contents_TB. This allows naming the scrollbar as well as specifying some of its properties. The vertical scrollbar is created by initialize_VScrollBar.
VScrollBar initialize_VScrollBar ( TextBox text_box )
{
VScrollBar vsb = new VScrollBar ( )
{
Cursor = Cursors.Arrow,
LargeChange =
( maximum_textbox_lines / 2 ),
Location = new Point (
( text_box.Width - VSB_WIDTH ),
0 ),
Maximum = int.MaxValue,
Minimum = 0,
Name = "textbox_VSB",
Size = new Size (
VSB_WIDTH,
( text_box.Height - 3 ) ),
SmallChange = 1,
Value = int.MaxValue
};
vsb.Scroll += new ScrollEventHandler ( TB_VSB_Scroll );
return ( vsb );
}
With textbox_VSB defined, all we need is to be notified of changes to the Vertical Scroll Bar controls (Line up or down, Page up or down). The event handler that provides these notifications is TB_VSB_Scroll.
5.4.2. TB_VSB_Scroll Event Handler
void TB_VSB_Scroll ( Object sender,
ScrollEventArgs e )
{
new_value = e.NewValue;
display_input_file ( );
}
6. Conclusion
This article has presented the revision to ViewFile, a tool that provides a byte-oriented display of file contents. The following figure presents an overview.
Although ViewFile is a useful tool, this article has attempted to illustrate how a number of event handlers can work together to provide a good user experience.
7. References
8. Development Environment
The software presented in this article was developed in the following environment:
Microsoft Windows 7 Professional Service Pack 1 |
Microsoft Visual Studio 2008 Professional |
Microsoft .Net Framework Version 3.5 SP1 |
Microsoft Visual C# 2008 |
9. History
ViewFile V4.0 | 12/16/2022 | Revised Tool and Deleted Head |
ViewFile V3.1 | 08/11/2017 | Revised Article and Software |
ViewFile V1.1 and Head V1.2 | 06/23/2014 | Original Article |