Introduction
This is a project where I had to manage a very, very large image without loading all the info to generate that image in memory.
It's like document programs work. In excel, you don't have an image of 1,000,000 x 20,000 cells.
The program goes to the file and "build" the screen image as you move. I had to maintain 2 versions (ATL and later .NET). I decided to use C++/CLI for the .NET version.
I adapted the code to load an image. It is difficult to work with scrollbars in .NET user controls. Specially if you want the control to work the way you want it to work.
This project includes:
Creating a .NET user control in C++/CLI
There are two different type of classes, native and managed classes. Managed classes are defined by "ref". For example:
private ref class CImgManager
Managed classes have references (with the ^ symbol), use the "gcnew" to create a new instance and can't have native classes as variables in the class declaration.
In this project, there are 3 managed classes:
- CImgManager: Contains the original image, the rectangle where the image is drawn, and the paint event
- CImgParams: Contains the zoom and image parameters (height, width, coordinates, etc)
- CScrollBarsMan: Contains code to manage scrollbars
For some reason, even after adding
using namespace System::Drawing
, declaring a Rectangle requires all the path. I think it's because there is a name conflict with Rectangle (Windows).
There are two events that the user control can fire:
- ImgOnMoveCoords: So that when the user moves the cursor on the control, can see the image coordinates.
- ImgOnScroll: This is used to refresh the zoom image with the rectangle. It fires when the zoom or coordinates change
The code to declare and use the events is in CLI_JPGViewerControl.
To declare events you need delegates:
public delegate void _ImgOnMoveCoords(int X, int Y);
public delegate void _ImgOnScroll();
And then define the events:
event _ImgOnMoveCoords^ ImgOnMoveCoords;
event _ImgOnScroll^ ImgOnScroll;
And finally, fire them when necessary.
There is usually no need to override the default message processing method of the user control, but in this case I need to get the scroll events:
protected: virtual void WndProc(Message% m) override
{
bool change = scrollman->MsgFromScroll(m);
if (m.Msg == WM_VSCROLL || m.Msg == WM_HSCROLL)
ImgOnScroll();
UserControl::WndProc( m );
}
The other methods to process mouse, resize or paint methods must be added to the events at InitializeComponent() by using the corresponding handlers:
this->Paint += gcnew System::Windows::Forms::PaintEventHandler(this, &CLI_JPGViewerControl::CLI_JPGViewerControl_Paint);
this->Resize += gcnew System::EventHandler(this, &CLI_JPGViewerControl::CLI_JPGViewerControl_Resize);
this->MouseMove += gcnew System::Windows::Forms::MouseEventHandler(this, &CLI_JPGViewerControl::CLI_JPGViewerControl_MouseMove);
this->MouseWheel += gcnew System::Windows::Forms::MouseEventHandler(this, &CLI_JPGViewerControl::CLI_JPGViewerControl_MouseWheel);
The managed class that deals with the image and drawing is
CImgManager:
Image^ ImgOrig | This is the image object where the image file is loaded |
Bitmap^ BmpRect | This is the bitmap where the image is copied, adjusting width or height depending of the width or height of the user control |
int ImgLeft, ImgTop; | This variables represent where the image starts in the bitmap. One of them will always be zero. |
Graphics^ grp; | This is the graphics object of the user control for the painting event |
System::Drawing::Rectangle^ rect; | This is the rectangle of the usercontrol where the class draws the image |
CImgParams^ imgparams; | Reference to zoom functionality |
Rectangle^ rdrawzoom; | This is the rectangle with the coordinates of zoom to draw the blue rectangle |
bool ShowRect; | This is to indicate if the image is one that shows zoom (blue rectangle) or not |
CImgManager() | Inits ImgOrig and BmpRect as null, creates the only instance of rdrawzoom |
void LoadImage(String^ strfilename); | Loads the image from file and builds the proportional bitmap |
void Paint(); | This is called from the paint event of the user control. It paints the zoomed image in the user control rectangle |
void MakeProportionalImage(); | This builds a bitmap proportional to the user control rectangle in BmpRect |
void RefreshSize(); | Recreates the bitmap when the size of the user control changes |
Point^ GetImgPointFromXY(Point^ pxy); | Gets the corresponding image coordinates from mouse location in the user control. If the mouse is in the blank area it returns (0,0) |
void Drawrectangle(); | Draws the zoom blue rectangle, based on the coordinates of rdrawzoom |
double Factor(int pWidth, int pHeight) | Gets the relationship between the two parameters to compare user control area vs image proportions |
The managed class that deals with image zoom is
CImgParams.Al the variables and methods are referred to the bitmap (proportional) image, not the original image.
double Zoom; | This is the variable for the class that is assigned when the user control zoom property changes |
int ImgHeight, ImgWidth; | These variables are the image height and width. They correspond to the altered image (proportional), that is, the bitmap |
int ImgLeft, ImgTop; | These variables are the top and left of the bitmap that indicate the begining of the image. They correspond to ImgLeft and ImgTop of CImgManager |
int MinZoomX, MaxZoomX, MinZoomY, MaxZoomY; | These are the coordinates of the zoom "square". They are related to the proportional bitmap (not the original image) |
Point^ GetZoomMiddlePoint(); | Returns the middle point of the zoom square. Useful to position the scrollbars |
Size^ GetZoomSize(); | Returns the size of the zoom square |
Rectangle^ GetImageZoomRectangle(); | Returns a rectangle corresponding to the square defined by the variables MinZoomX, MaxZoomX, MinZoomY, MaxZoomY |
void DoZoom(); | From the image coordinates, it assigns MinZoomX, MaxZoomX, MinZoomY, MaxZoomY from the zoom variable, centered |
void DoZoomFromPoint(); | The same as DoZoom, but after zooming, it moves the center of the square to where it was before |
void Init(int ImgWidth, int ImgHeight); | Initializes the variables to zoom 1 (including the square). |
The scrollbars are positioned and resized used mostly unmanaged code that calls windows APIs. This is mostly because the code is copied from ATL controls.
One important thing is that scrollbars don't get updated correctly when you change their size, event after the method Invalidate(), so a call to SetWindowPos must be done.
When setting the zoom in the user control:
Invalidate();
HWND pHwnd = (HWND)Handle.ToPointer();
SetWindowPos(pHwnd, 0,0,0,0,0,SWP_NOMOVE | SWP_NOSIZE | SWP_FRAMECHANGED);
All the code to handle scrollbars is in the
CScrollBarsMan:
UserControl^ usrctl; | This is a reference to the user control. It is used to get or set the .NET scrollbars, and to get the handle of the window |
CImgParams^ imgparams; | This is a reference to the zooming functionality. In order to position scrollbars, it is necesary to know the zoom state and coordinates |
bool UseScrollBars; | If scrollbars appear (visible) |
void SetScrollBars(); | Sets the scrollbars size and visibility. It is called when the user zooms in or out (not when he moves) |
void SetScrollBarsPos; | Positions the scrollbars in the center. This method is not used. |
void PosScroll(int horzvert, int pos); | Positions the scrollbar in a point. It is only called by SetScrollBarsPos, which is not used. |
Point^ GetZoomRelToTotal(); | Returns the left top point of the zoom square. This method is not used |
void RefreshParamsFromScroll(); | Refresh the zoom rectangle based on scroll input |
int GetScrollPosition(int horzvert); horzvert); | Gets the absolute scroll position |
void UpdateScrollsFromControl(); | Updates the scrollbar positions (both) base on the central point of the zoom rectangle. |
bool MsgFromScroll(Message% m); | This is the call from the WndProc of the user control, and manages the message. |
template<unsigned Min, unsigned Max, unsigned LineP, unsigned LineM, unsigned PageP, unsigned PageM> void ProcMsg( int PosInfo, int horzvert, UINT nSBCode); | This method handles the message. As vertical and horizontal movements are similar, a template is used to not repeat code. |
int PosX, PosY; | Hold the last values of the scrollbars to see if a change is made. |
Rectangle^ rdrawzoom; | This is the rectangle of the zoom. It is used to send to another control (the one with the blue box) the coordinates to draw it. The blue box "changes" when the user scrolls or zooms. |
void UpdateRectZoom(); | This is the method that updates the rectangle zoom. |
template<typename T> void SetScrollUsrCtrl(T^ scroll, int Total, int Change); | Method to set the scrollbars after a zoom change. |
void UpdateScrolls(int horzvert, int posmiddle, int PosInfo); | Updates a single scroll from zoom (called from UpdateScrollsFromControl). |
To do
- Remove bitmap and allow loading original image only.
- Improve zoom.
- Add pan and rectangles.