///////////////////////////////////////////////////////////////////////////////////////////////////
|
// Copyright 2012 Advanced Software Engineering Limited
|
//
|
// You may use and modify the code in this file in your application, provided the code and
|
// its modifications are used only in conjunction with ChartDirector. Usage of this software
|
// is subjected to the terms and condition of the ChartDirector license.
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
// CChartViewer Implementation
|
//
|
// The CChartViewer is a subclass of CStatic for displaying chart images. It extends CStatic
|
// to support to support image maps, clickable hot spots with tool tips, zooming and scrolling
|
// support and image update rate control.
|
//
|
// To use CChartViewer, create the layout template using the Resource Editor with CStatic
|
// (Picture) controls configured to display bitmaps. Remember to check the "Notify" check
|
// box for the control. Create member variables for them using the ClassWizard as usual.
|
// Then edit the header file to include "ChartViewer.h" and change CStatic to CChartViewer.
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
|
#include "stdafx.h"
|
#include "ChartViewer.h"
|
|
#ifdef _DEBUG
|
#define new DEBUG_NEW
|
#endif
|
|
/////////////////////////////////////////////////////////////////////////////
|
// CChartViewer
|
|
//
|
// Build in mouse cursors for zooming and scrolling support
|
//
|
static HCURSOR getZoomInCursor();
|
static HCURSOR getZoomOutCursor();
|
static HCURSOR getNoZoomCursor();
|
static HCURSOR getNoMove2DCursor();
|
static HCURSOR getNoMoveHorizCursor();
|
static HCURSOR getNoMoveVertCursor();
|
|
//
|
// Constants used in m_delayChartUpdate
|
//
|
enum DelayNeedType { NO_DELAY, NEED_DELAY, NEED_UPDATE };
|
enum UpdateViewType { UPDATE_VIEW_PORT_TIMER = 1, DELAYED_MOUSE_MOVE_TIMER = 2 };
|
|
|
//
|
// Constructor
|
//
|
CChartViewer::CChartViewer()
|
{
|
// current chart and hot spot tester
|
m_currentChart = 0;
|
m_hotSpotTester = 0;
|
|
// create the tool tip control
|
m_ToolTip.Create(this);
|
m_ToolTip.Activate(TRUE);
|
m_ToolTip.ModifyStyle(0, TTS_NOPREFIX);
|
m_toolTipHasAttached = false;
|
|
// initialize chart configuration
|
m_selectBoxLineColor = RGB(0, 0, 0);
|
m_selectBoxLineWidth = 2;
|
m_mouseUsage = Chart::MouseUsageDefault;
|
m_zoomDirection = Chart::DirectionHorizontal;
|
m_zoomInRatio = 2;
|
m_zoomOutRatio = 0.5;
|
m_scrollDirection = Chart::DirectionHorizontal;
|
m_minDragAmount = 5;
|
m_updateInterval = 20;
|
|
// current state of the mouse
|
m_isOnPlotArea = false;
|
m_isPlotAreaMouseDown = false;
|
m_isDragScrolling = false;
|
m_currentHotSpot = -1;
|
m_isClickable = false;
|
m_isMouseTracking = false;
|
m_isInMouseMove = false;
|
|
// chart update rate support
|
m_needUpdateChart = false;
|
m_needUpdateImageMap = false;
|
m_holdTimerActive = false;
|
m_delayUpdateChart = NO_DELAY;
|
m_delayedChart = 0;
|
m_lastMouseMove = 0;
|
m_hasDelayedMouseMove = false;
|
|
// track cursor support
|
m_autoHideMsg = 0;
|
m_currentMouseX = 0;
|
m_currentMouseY = 0;
|
m_isInMouseMovePlotArea = false;
|
}
|
|
BEGIN_MESSAGE_MAP(CChartViewer, CStatic)
|
//{{AFX_MSG_MAP(CChartViewer)
|
ON_WM_MOUSEMOVE()
|
ON_MESSAGE(WM_MOUSELEAVE, OnMouseLeave)
|
ON_WM_SETCURSOR()
|
ON_WM_DESTROY()
|
ON_WM_LBUTTONDOWN()
|
ON_WM_LBUTTONUP()
|
ON_WM_TIMER()
|
//}}AFX_MSG_MAP
|
END_MESSAGE_MAP()
|
|
/////////////////////////////////////////////////////////////////////////////
|
// CChartViewer message handlers
|
|
//
|
// Free resources
|
//
|
void CChartViewer::OnDestroy()
|
{
|
CStatic::OnDestroy();
|
|
// Free HBITMAP
|
HBITMAP b = SetBitmap(0);
|
if (0 != b)
|
DeleteObject(b);
|
|
// Free hot spot tester
|
if (0 != m_hotSpotTester)
|
delete m_hotSpotTester;
|
m_hotSpotTester = 0;
|
}
|
|
//
|
// MouseMove event handler
|
//
|
void CChartViewer::OnMouseMove(UINT nFlags, CPoint point)
|
{
|
// Enable mouse tracking to detect mouse leave events
|
if (!m_isMouseTracking)
|
{
|
TRACKMOUSEEVENT e;
|
e.cbSize = sizeof(e);
|
e.dwFlags = TME_LEAVE;
|
e.hwndTrack = this->m_hWnd;
|
m_isMouseTracking = (0 != TrackMouseEvent(&e));
|
}
|
|
// On Windows, mouse events can by-pass the event queue. If there are too many mouse events,
|
// the event queue may not get processed, preventing other controls from updating. If two mouse
|
// events are less than 10ms apart, there is a risk of too many mouse events. So we repost the
|
// mouse event as a timer event that is queued up normally, allowing the queue to get processed.
|
unsigned int timeBetweenMouseMove = GetTickCount() - m_lastMouseMove;
|
if ((m_hasDelayedMouseMove && (timeBetweenMouseMove < 250)) || (timeBetweenMouseMove < 10))
|
{
|
m_delayedMouseMoveFlag = nFlags;
|
m_delayedMouseMovePoint = point;
|
if (!m_hasDelayedMouseMove)
|
{
|
m_hasDelayedMouseMove = true;
|
SetTimer(DELAYED_MOUSE_MOVE_TIMER, 1, 0);
|
}
|
}
|
else
|
commitMouseMove(nFlags, point);
|
}
|
|
//
|
// The method that actually performs MouseMove event processing
|
//
|
void CChartViewer::commitMouseMove(UINT nFlags, CPoint point)
|
{
|
// Cancel the delayed mouse event if any
|
if (m_hasDelayedMouseMove)
|
{
|
KillTimer(DELAYED_MOUSE_MOVE_TIMER);
|
m_hasDelayedMouseMove = false;
|
}
|
|
// Remember the mouse coordinates for later use
|
m_currentMouseX = point.x;
|
m_currentMouseY = point.y;
|
|
// The chart can be updated more than once during mouse move. For example, it can update due to
|
// drag to scroll, and also due to drawing track cursor. So we delay updating the display until
|
// all all events has occured.
|
m_delayUpdateChart = NEED_DELAY;
|
m_isInMouseMove = true;
|
|
// Check if mouse is dragging on the plot area
|
m_isOnPlotArea = m_isPlotAreaMouseDown || inPlotArea(point.x, point.y);
|
if (m_isPlotAreaMouseDown)
|
OnPlotAreaMouseDrag(nFlags, point);
|
|
// Send CVN_MouseMoveChart
|
GetParent()->SendMessage(WM_COMMAND, MAKEWPARAM(GetDlgCtrlID(), CVN_MouseMoveChart),
|
(LPARAM)m_hWnd);
|
|
if (inExtendedPlotArea(point.x, point.y))
|
{
|
// Mouse is in extended plot area, send CVN_MouseMovePlotArea
|
m_isInMouseMovePlotArea = true;
|
GetParent()->SendMessage(WM_COMMAND, MAKEWPARAM(GetDlgCtrlID(), CVN_MouseMovePlotArea),
|
(LPARAM)m_hWnd);
|
}
|
else if (m_isInMouseMovePlotArea)
|
{
|
// Mouse was in extended plot area, but is not in it now, so send CVN_MouseLeavePlotArea
|
m_isInMouseMovePlotArea = false;
|
GetParent()->SendMessage(WM_COMMAND, MAKEWPARAM(GetDlgCtrlID(), CVN_MouseLeavePlotArea),
|
(LPARAM)m_hWnd);
|
applyAutoHide(CVN_MouseLeavePlotArea);
|
}
|
|
// Can update chart now
|
commitUpdateChart();
|
m_isInMouseMove = false;
|
|
//
|
// Show hot spot tool tips if necessary
|
//
|
if (!m_toolTipHasAttached)
|
{
|
m_toolTipHasAttached = true;
|
|
// Connects the CChartViewer to the CToolTipCtrl control
|
m_ToolTip.AddTool(this);
|
m_ToolTip.SendMessage(TTM_SETMAXTIPWIDTH, 0, SHRT_MAX);
|
}
|
|
if (nFlags != 0)
|
{
|
// Hide tool tips if mouse button is pressed.
|
m_ToolTip.UpdateTipText(_T(""), this);
|
}
|
else
|
{
|
// Use the ChartDirector ImageMapHandler to determine if the mouse is over a hot spot
|
int hotSpotNo = 0;
|
if (0 != m_hotSpotTester)
|
hotSpotNo = m_hotSpotTester->getHotSpot(point.x, point.y);
|
|
// If the mouse is in the same hot spot since the last mouse move event, there is no need
|
// to update the tool tip.
|
if (hotSpotNo != m_currentHotSpot)
|
{
|
// Hot spot has changed - update tool tip text
|
m_currentHotSpot = hotSpotNo;
|
|
if (hotSpotNo == 0)
|
{
|
// Mouse is not actually on hanlder hot spot - use default tool tip text and reset
|
// the clickable flag.
|
m_ToolTip.UpdateTipText(m_defaultToolTip, this);
|
m_isClickable = false;
|
}
|
else
|
{
|
// Mouse is on a hot spot. In this implementation, we consider the hot spot as
|
// clickable if its href ("path") parameter is not empty.
|
const char *path = m_hotSpotTester->getValue("path");
|
m_isClickable = ((0 != path) && (0 != *path));
|
|
// Use the title attribute as the tool tip. Note that ChartDirector uses UTF8,
|
// while MFC uses TCHAR, so we use the utility UTF8toTCHAR for conversion.
|
m_ToolTip.UpdateTipText(UTF8toTCHAR(m_hotSpotTester->getValue("title")), this);
|
}
|
}
|
}
|
|
m_lastMouseMove = GetTickCount();
|
CStatic::OnMouseMove(nFlags, point);
|
}
|
|
//
|
// Delayed MouseMove event handler
|
//
|
void CChartViewer::OnDelayedMouseMove()
|
{
|
if (m_hasDelayedMouseMove)
|
commitMouseMove(m_delayedMouseMoveFlag, m_delayedMouseMovePoint);
|
}
|
|
//
|
// MouseLeave event handler
|
//
|
LRESULT CChartViewer::OnMouseLeave(WPARAM wParam, LPARAM lParam)
|
{
|
// Process delayed mouse move, if any
|
OnDelayedMouseMove();
|
|
// Mouse tracking is no longer active
|
m_isMouseTracking = false;
|
|
if (m_isInMouseMovePlotArea)
|
{
|
// Mouse was in extended plot area, but is not in it now, so send CVN_MouseLeavePlotArea
|
m_isInMouseMovePlotArea = false;
|
GetParent()->SendMessage(WM_COMMAND, MAKEWPARAM(GetDlgCtrlID(), CVN_MouseLeavePlotArea),
|
(LPARAM)m_hWnd);
|
applyAutoHide(CVN_MouseLeavePlotArea);
|
}
|
|
// Send CVN_MouseLeaveChart
|
GetParent()->SendMessage(WM_COMMAND, MAKEWPARAM(GetDlgCtrlID(), CVN_MouseLeaveChart),
|
(LPARAM)m_hWnd);
|
applyAutoHide(CVN_MouseLeaveChart);
|
|
return TRUE;
|
}
|
|
//
|
// Intercept WM_SETCURSOR to change the mouse cursor.
|
//
|
BOOL CChartViewer::OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message)
|
{
|
if (m_isOnPlotArea)
|
{
|
switch (m_mouseUsage)
|
{
|
case Chart::MouseUsageZoomIn:
|
if (canZoomIn(m_zoomDirection))
|
::SetCursor(getZoomInCursor());
|
else
|
::SetCursor(getNoZoomCursor());
|
return TRUE;
|
case Chart::MouseUsageZoomOut:
|
if (canZoomOut(m_zoomDirection))
|
::SetCursor(getZoomOutCursor());
|
else
|
::SetCursor(getNoZoomCursor());
|
return TRUE;
|
}
|
}
|
|
if (m_isClickable)
|
{
|
// Hand cursor = IDC_HAND = 32649
|
HCURSOR h = AfxGetApp()->LoadStandardCursor(MAKEINTRESOURCE(32649));
|
if (NULL != h)
|
::SetCursor(h);
|
return TRUE;
|
}
|
|
return CStatic::OnSetCursor(pWnd, nHitTest, message);
|
}
|
|
//
|
// Mouse left button down event.
|
//
|
void CChartViewer::OnLButtonDown(UINT nFlags, CPoint point)
|
{
|
OnDelayedMouseMove();
|
|
if (inPlotArea(point.x, point.y) && (m_mouseUsage != Chart::MouseUsageDefault))
|
{
|
// Mouse usage is for drag to zoom/scroll. Capture the mouse to prepare for dragging and
|
// save the mouse down position to draw the selection rectangle.
|
SetCapture();
|
m_isPlotAreaMouseDown = true;
|
m_plotAreaMouseDownXPos = point.x;
|
m_plotAreaMouseDownYPos = point.y;
|
startDrag();
|
}
|
else
|
CStatic::OnLButtonDown(nFlags, point);
|
}
|
|
//
|
// Mouse left button up event.
|
//
|
void CChartViewer::OnLButtonUp(UINT nFlags, CPoint point)
|
{
|
OnDelayedMouseMove();
|
|
if (m_isPlotAreaMouseDown)
|
{
|
// Release the mouse capture.
|
ReleaseCapture();
|
m_isPlotAreaMouseDown = false;
|
setRectVisible(false);
|
bool hasUpdate = false;
|
|
switch (m_mouseUsage)
|
{
|
case Chart::MouseUsageZoomIn :
|
if (canZoomIn(m_zoomDirection))
|
{
|
if (isDrag(m_zoomDirection, point))
|
// Zoom to the drag selection rectangle.
|
hasUpdate = zoomTo(m_zoomDirection,
|
m_plotAreaMouseDownXPos, m_plotAreaMouseDownYPos, point.x, point.y);
|
else
|
// User just click on a point. Zoom-in around the mouse cursor position.
|
hasUpdate = zoomAt(m_zoomDirection, point.x, point.y, m_zoomInRatio);
|
}
|
break;
|
case Chart::MouseUsageZoomOut:
|
// Zoom out around the mouse cursor position.
|
if (canZoomOut(m_zoomDirection))
|
hasUpdate = zoomAt(m_zoomDirection, point.x, point.y, m_zoomOutRatio);
|
break;
|
default :
|
if (m_isDragScrolling)
|
// Drag to scroll. We can update the image map now as scrolling has finished.
|
updateViewPort(false, true);
|
else
|
// Is not zooming or scrolling, so is just a normal click event.
|
GetParent()->SendMessage(WM_COMMAND, MAKEWPARAM(GetDlgCtrlID(), BN_CLICKED), (LPARAM)m_hWnd);
|
break;
|
}
|
|
m_isDragScrolling = false;
|
if (hasUpdate)
|
// View port has changed - update it.
|
updateViewPort(true, true);
|
}
|
else
|
CStatic::OnLButtonUp(nFlags, point);
|
}
|
|
//
|
// Chart hold timer.
|
//
|
void CChartViewer::OnTimer(UINT_PTR nIDEvent)
|
{
|
if (nIDEvent == DELAYED_MOUSE_MOVE_TIMER)
|
{
|
// Is a delayed mouse move event
|
OnDelayedMouseMove();
|
}
|
else
|
{
|
// Is a delayed view port update
|
m_holdTimerActive = false;
|
|
// Reset the timer
|
KillTimer(nIDEvent);
|
|
// If has pending chart view port update request, handles them now.
|
if (m_needUpdateChart || m_needUpdateImageMap)
|
updateViewPort(m_needUpdateChart, m_needUpdateImageMap);
|
}
|
}
|
|
/////////////////////////////////////////////////////////////////////////////
|
// CChartViewer overrides
|
|
BOOL CChartViewer::PreTranslateMessage(MSG* pMsg)
|
{
|
// Remember to forward mouse messages to the CToolTipCtrl
|
BOOL res = CStatic::PreTranslateMessage(pMsg);
|
m_ToolTip.RelayEvent(pMsg);
|
return res;
|
}
|
|
/////////////////////////////////////////////////////////////////////////////
|
// CChartViewer properties
|
|
//
|
// Set the chart to the control
|
//
|
void CChartViewer::setChart(BaseChart *c)
|
{
|
// In case the user forgets to check the "Notify" check box in the Dialog editor, we set it
|
// ourselves so the CChartViewer control can receive mouse events.
|
if ((GetStyle() & SS_NOTIFY) == 0)
|
ModifyStyle(0, SS_NOTIFY);
|
|
m_currentChart = c;
|
setImageMap(0);
|
|
if (0 != c)
|
{
|
commitPendingSyncAxis(c);
|
if (m_delayUpdateChart != NO_DELAY)
|
c->makeChart();
|
}
|
|
updateDisplay();
|
}
|
|
//
|
// Get back the same BaseChart pointer provided by the previous setChart call.
|
//
|
BaseChart *CChartViewer::getChart()
|
{
|
return m_currentChart;
|
}
|
|
//
|
// Set image map used by the chart
|
//
|
void CChartViewer::setImageMap(const char *imageMap)
|
{
|
//delete the existing ImageMapHandler
|
if (0 != m_hotSpotTester)
|
delete m_hotSpotTester;
|
m_currentHotSpot = -1;
|
m_isClickable = false;
|
|
//create a new ImageMapHandler to represent the image map
|
if ((0 == imageMap) || (0 == *imageMap))
|
m_hotSpotTester = 0;
|
else
|
m_hotSpotTester = new ImageMapHandler(imageMap);
|
}
|
|
//
|
// Get the image map handler for the chart
|
//
|
ImageMapHandler *CChartViewer::getImageMapHandler()
|
{
|
return m_hotSpotTester;
|
}
|
|
//
|
// Set the default tool tip to use
|
//
|
void CChartViewer::setDefaultToolTip(LPCTSTR text)
|
{
|
m_defaultToolTip = text;
|
}
|
|
//
|
// Get the CToolTipCtrl for managing tool tips.
|
//
|
CToolTipCtrl *CChartViewer::getToolTipCtrl()
|
{
|
return &m_ToolTip;
|
}
|
|
//
|
// Set the border width of the selection box
|
//
|
void CChartViewer::setSelectionBorderWidth(int width)
|
{
|
m_selectBoxLineWidth = width;
|
}
|
|
//
|
// Get the border with of the selection box.
|
//
|
int CChartViewer::getSelectionBorderWidth()
|
{
|
return m_selectBoxLineWidth;
|
}
|
|
//
|
// Set the border color of the selection box
|
//
|
void CChartViewer::setSelectionBorderColor(COLORREF c)
|
{
|
m_selectBoxLineColor = c;
|
if (m_TopLine.m_hWnd != 0)
|
{
|
m_TopLine.SetColor(c);
|
m_LeftLine.SetColor(c);
|
m_BottomLine.SetColor(c);
|
m_RightLine.SetColor(c);
|
}
|
}
|
|
//
|
// Get the border color of the selection box.
|
//
|
COLORREF CChartViewer::getSelectionBorderColor()
|
{
|
return m_selectBoxLineColor;
|
}
|
|
//
|
// Set the mouse usage mode
|
//
|
void CChartViewer::setMouseUsage(int mouseUsage)
|
{
|
m_mouseUsage = mouseUsage;
|
}
|
|
//
|
// Get the mouse usage mode
|
//
|
int CChartViewer::getMouseUsage()
|
{
|
return m_mouseUsage;
|
}
|
|
//
|
// Set the zoom direction
|
//
|
void CChartViewer::setZoomDirection(int direction)
|
{
|
m_zoomDirection = direction;
|
}
|
|
//
|
// Get the zoom direction
|
//
|
int CChartViewer::getZoomDirection()
|
{
|
return m_zoomDirection;
|
}
|
|
//
|
// Set the scroll direction
|
//
|
void CChartViewer::setScrollDirection(int direction)
|
{
|
m_scrollDirection = direction;
|
}
|
|
//
|
// Get the scroll direction
|
//
|
int CChartViewer::getScrollDirection()
|
{
|
return m_scrollDirection;
|
}
|
|
//
|
// Set the zoom-in ratio for mouse click zoom-in
|
//
|
void CChartViewer::setZoomInRatio(double ratio)
|
{
|
m_zoomInRatio = ratio;
|
}
|
|
//
|
// Get the zoom-in ratio for mouse click zoom-in
|
//
|
double CChartViewer::getZoomInRatio()
|
{
|
return m_zoomInRatio;
|
}
|
|
//
|
// Set the zoom-out ratio
|
//
|
void CChartViewer::setZoomOutRatio(double ratio)
|
{
|
m_zoomOutRatio = ratio;
|
}
|
|
//
|
// Get the zoom-out ratio
|
//
|
double CChartViewer::getZoomOutRatio()
|
{
|
return m_zoomOutRatio;
|
}
|
|
//
|
// Set the minimum mouse drag before the dragging is considered as real. This is to avoid small
|
// mouse vibrations triggering a mouse drag.
|
//
|
void CChartViewer::setMinimumDrag(int offset)
|
{
|
m_minDragAmount = offset;
|
}
|
|
//
|
// Get the minimum mouse drag before the dragging is considered as real.
|
//
|
int CChartViewer::getMinimumDrag()
|
{
|
return m_minDragAmount;
|
}
|
|
//
|
// Set the minimum interval between ViewPortChanged events. This is to avoid the chart being
|
// updated too frequently. (Default is 20ms between chart updates.) Multiple update events
|
// arrived during the interval will be merged into one chart update and executed at the end
|
// of the interval.
|
//
|
void CChartViewer::setUpdateInterval(int interval)
|
{
|
m_updateInterval = interval;
|
}
|
|
//
|
// Get the minimum interval between ViewPortChanged events.
|
//
|
int CChartViewer::getUpdateInterval()
|
{
|
return m_updateInterval;
|
}
|
|
//
|
// Check if there is a pending chart update request.
|
//
|
bool CChartViewer::needUpdateChart()
|
{
|
return m_needUpdateChart;
|
}
|
|
//
|
// Check if there is a pending image map update request.
|
//
|
bool CChartViewer::needUpdateImageMap()
|
{
|
return m_needUpdateImageMap;
|
}
|
|
//
|
// Get the current mouse x coordinate when used in a mouse move event handler
|
//
|
int CChartViewer::getChartMouseX()
|
{
|
return m_currentMouseX;
|
}
|
|
//
|
// Get the current mouse y coordinate when used in a mouse move event handler
|
//
|
int CChartViewer::getChartMouseY()
|
{
|
return m_currentMouseY;
|
}
|
|
//
|
// Get the current mouse x coordinate bounded to the plot area when used in a mouse event handler
|
//
|
int CChartViewer::getPlotAreaMouseX()
|
{
|
int ret = getChartMouseX();
|
if (ret < getPlotAreaLeft())
|
ret = getPlotAreaLeft();
|
if (ret > getPlotAreaLeft() + getPlotAreaWidth())
|
ret = getPlotAreaLeft() + getPlotAreaWidth();
|
return ret;
|
}
|
|
//
|
// Get the current mouse y coordinate bounded to the plot area when used in a mouse event handler
|
//
|
int CChartViewer::getPlotAreaMouseY()
|
{
|
int ret = getChartMouseY();
|
if (ret < getPlotAreaTop())
|
ret = getPlotAreaTop();
|
if (ret > getPlotAreaTop() + getPlotAreaHeight())
|
ret = getPlotAreaTop() + getPlotAreaHeight();
|
return ret;
|
}
|
|
//
|
// Check if mouse is on the extended plot area
|
//
|
bool CChartViewer::isMouseOnPlotArea()
|
{
|
if (this->m_isMouseTracking)
|
return inExtendedPlotArea(getChartMouseX(), getChartMouseY());
|
else
|
return false;
|
}
|
|
//
|
// Check if is currently processing a mouse move event
|
//
|
bool CChartViewer::isInMouseMoveEvent()
|
{
|
return m_isInMouseMove;
|
}
|
|
/////////////////////////////////////////////////////////////////////////////
|
// CChartViewer methods
|
|
//
|
// Update the display
|
//
|
void CChartViewer::updateDisplay()
|
{
|
if (m_delayUpdateChart == NO_DELAY)
|
commitUpdateChart();
|
else
|
{
|
m_delayUpdateChart = NEED_UPDATE;
|
delete m_delayedChart;
|
m_delayedChart = (0 != m_currentChart) ? new BaseChart(m_currentChart) : 0;
|
}
|
}
|
|
//
|
// Commit chart to display
|
//
|
void CChartViewer::commitUpdateChart()
|
{
|
if (m_delayUpdateChart == NEED_DELAY)
|
{
|
// No actual update occur
|
m_delayUpdateChart = NO_DELAY;
|
return;
|
}
|
|
// Get the HBITMAP and metrics for the chart
|
BaseChart *c = (m_delayUpdateChart == NEED_UPDATE) ? m_delayedChart : m_currentChart;
|
HBITMAP chartBMP = 0;
|
const char *chartMetrics = 0;
|
|
if (0 != c)
|
{
|
// Output chart as Device Indpendent Bitmap with file headers
|
MemBlock m = c->makeChart(Chart::BMP);
|
|
// MFC expects HBITMAP, so convert from DIB to HBITMAP.
|
CDC *cdc = GetDC();
|
chartBMP = CreateDIBitmap(
|
cdc->m_hDC,
|
(const struct tagBITMAPINFOHEADER *)(m.data + 14),
|
CBM_INIT,
|
m.data + *(int *)(m.data + 10),
|
(const struct tagBITMAPINFO *)(m.data + 14),
|
DIB_RGB_COLORS);
|
ReleaseDC(cdc);
|
|
// Get chart metrics
|
chartMetrics = c->getChartMetrics();
|
}
|
|
// Set the HBITMAP for display
|
if (0 != (chartBMP = SetBitmap(chartBMP)))
|
DeleteObject(chartBMP);
|
|
// Set the chart metrics and clear the image map
|
setChartMetrics(chartMetrics);
|
|
// Remember to update the selection rectangle
|
if ((0 != m_TopLine.m_hWnd) && m_TopLine.IsWindowVisible())
|
{
|
m_TopLine.Invalidate();
|
m_LeftLine.Invalidate();
|
m_RightLine.Invalidate();
|
m_BottomLine.Invalidate();
|
m_TopLine.UpdateWindow();
|
m_LeftLine.UpdateWindow();
|
m_RightLine.UpdateWindow();
|
m_BottomLine.UpdateWindow();
|
}
|
|
// Any delayed chart has been committed
|
m_delayUpdateChart = NO_DELAY;
|
delete m_delayedChart;
|
m_delayedChart = 0;
|
}
|
|
//
|
// Set the message used to remove the dynamic layer
|
//
|
void CChartViewer::removeDynamicLayer(int msg)
|
{
|
m_autoHideMsg = msg;
|
if (msg == -1)
|
applyAutoHide(msg);
|
}
|
|
//
|
// Attempt to hide the dynamic layer using the specified message
|
//
|
void CChartViewer::applyAutoHide(int msg)
|
{
|
if (m_autoHideMsg == msg)
|
{
|
if (0 != m_currentChart)
|
m_currentChart->removeDynamicLayer();
|
m_autoHideMsg = 0;
|
|
updateDisplay();
|
}
|
}
|
|
//
|
// Create the edges for the selection rectangle
|
//
|
void CChartViewer::initRect()
|
{
|
m_TopLine.Create(GetParent(), m_selectBoxLineColor);
|
m_LeftLine.Create(GetParent(), m_selectBoxLineColor);
|
m_BottomLine.Create(GetParent(), m_selectBoxLineColor);
|
m_RightLine.Create(GetParent(), m_selectBoxLineColor);
|
}
|
|
//
|
// Set selection rectangle position and size
|
//
|
void CChartViewer::drawRect(int x, int y, int width, int height)
|
{
|
// Create the edges of the rectangle if not already created
|
if (m_TopLine.m_hWnd == 0)
|
initRect();
|
|
// width < 0 is interpreted as the rectangle extends to the left or x.
|
// height <0 is interpreted as the rectangle extends to above y.
|
if (width < 0)
|
x -= (width = -width);
|
if (height < 0)
|
y -= (height = -height);
|
|
// Compute the position of the selection rectangle as relative to the parent window
|
RECT rect;
|
rect.left = x;
|
rect.top = y;
|
rect.right = x + width;
|
rect.bottom = y + height;
|
MapWindowPoints(m_TopLine.GetParent(), &rect);
|
|
// Put the edges along the sides of the rectangle
|
m_TopLine.MoveWindow(rect.left, rect.top, rect.right - rect.left, m_selectBoxLineWidth);
|
m_LeftLine.MoveWindow(rect.left, rect.top, m_selectBoxLineWidth, rect.bottom - rect.top);
|
m_BottomLine.MoveWindow(rect.left, rect.bottom - m_selectBoxLineWidth + 1,
|
rect.right - rect.left, m_selectBoxLineWidth);
|
m_RightLine.MoveWindow(rect.right - m_selectBoxLineWidth + 1, rect.top,
|
m_selectBoxLineWidth, rect.bottom - rect.top);
|
}
|
|
//
|
// Show/hide selection rectangle
|
//
|
void CChartViewer::setRectVisible(bool b)
|
{
|
// Create the edges of the rectangle if not already created
|
if (b && (m_TopLine.m_hWnd == 0))
|
initRect();
|
|
// Show/hide the edges
|
if (m_TopLine.m_hWnd != 0)
|
{
|
int state = b ? SW_SHOW : SW_HIDE;
|
m_TopLine.ShowWindow(state);
|
m_LeftLine.ShowWindow(state);
|
m_BottomLine.ShowWindow(state);
|
m_RightLine.ShowWindow(state);
|
}
|
}
|
|
//
|
// Determines if the mouse is dragging.
|
//
|
bool CChartViewer::isDrag(int direction, CPoint point)
|
{
|
// We only consider the mouse is dragging it is has dragged more than m_minDragAmount. This is
|
// to avoid small mouse vibrations triggering a mouse drag.
|
int spanX = abs(point.x - m_plotAreaMouseDownXPos);
|
int spanY = abs(point.y - m_plotAreaMouseDownYPos);
|
return ((direction != Chart::DirectionVertical) && (spanX >= m_minDragAmount)) ||
|
((direction != Chart::DirectionHorizontal) && (spanY >= m_minDragAmount));
|
}
|
|
//
|
// Process mouse dragging over the plot area
|
//
|
void CChartViewer::OnPlotAreaMouseDrag(UINT /* nFlags */, CPoint point)
|
{
|
switch (m_mouseUsage)
|
{
|
case Chart::MouseUsageZoomIn :
|
{
|
//
|
// Mouse is used for zoom in. Draw the selection rectangle if necessary.
|
//
|
|
bool isDragZoom = canZoomIn(m_zoomDirection) && isDrag(m_zoomDirection, point);
|
if (isDragZoom)
|
{
|
int spanX = m_plotAreaMouseDownXPos - point.x;
|
int spanY = m_plotAreaMouseDownYPos - point.y;
|
|
switch (m_zoomDirection)
|
{
|
case Chart::DirectionHorizontal:
|
drawRect(point.x, getPlotAreaTop(), spanX, getPlotAreaHeight());
|
break;
|
case Chart::DirectionVertical:
|
drawRect(getPlotAreaLeft(), point.y, getPlotAreaWidth(), spanY);
|
break;
|
default:
|
drawRect(point.x, point.y, spanX, spanY);
|
break;
|
}
|
}
|
setRectVisible(isDragZoom);
|
break;
|
}
|
case Chart::MouseUsageScroll :
|
{
|
//
|
// Mouse is used for drag scrolling. Scroll and update the view port.
|
//
|
|
if (m_isDragScrolling || isDrag(m_scrollDirection, point))
|
{
|
m_isDragScrolling = true;
|
switch (m_scrollDirection)
|
{
|
case Chart::DirectionHorizontal:
|
::SetCursor(getNoMoveHorizCursor());
|
break;
|
case Chart::DirectionVertical:
|
::SetCursor(getNoMoveVertCursor());
|
break;
|
default :
|
::SetCursor(getNoMove2DCursor());
|
break;
|
}
|
|
if (dragTo(m_scrollDirection,
|
point.x - m_plotAreaMouseDownXPos, point.y - m_plotAreaMouseDownYPos))
|
updateViewPort(true, false);
|
}
|
}
|
}
|
}
|
|
//
|
// Trigger the ViewPortChanged event
|
//
|
void CChartViewer::updateViewPort(bool needUpdateChart, bool needUpdateImageMap)
|
{
|
// Merge the current update requests with any pending requests.
|
m_needUpdateChart = m_needUpdateChart || needUpdateChart;
|
m_needUpdateImageMap = needUpdateImageMap;
|
|
// Hold timer has not expired, so do not update chart immediately. Keep the requests pending.
|
if (m_holdTimerActive)
|
return;
|
|
// The chart can be updated more than once during mouse move. For example, it can update due to
|
// drag to scroll, and also due to drawing track cursor. So we delay updating the display until
|
// all all updates has occured.
|
int hasDelayUpdate = (m_delayUpdateChart != NO_DELAY);
|
if (!hasDelayUpdate)
|
m_delayUpdateChart = NEED_DELAY;
|
|
// Can trigger the ViewPortChanged event.
|
validateViewPort();
|
GetParent()->SendMessage(WM_COMMAND, MAKEWPARAM(GetDlgCtrlID(), CVN_ViewPortChanged),
|
(LPARAM)m_hWnd);
|
|
// Can update chart now
|
if (!hasDelayUpdate)
|
commitUpdateChart();
|
|
// Clear any pending updates.
|
m_needUpdateChart = false;
|
m_needUpdateImageMap = false;
|
|
// Set hold timer to prevent multiple chart updates within a short period.
|
if (m_updateInterval > 0)
|
{
|
m_holdTimerActive = true;
|
SetTimer(UPDATE_VIEW_PORT_TIMER, m_updateInterval, 0);
|
}
|
}
|
|
/////////////////////////////////////////////////////////////////////////////
|
// CRectCtrl
|
//
|
// A rectangle with a background color. Use as thick edges for the selection
|
// rectangle.
|
//
|
|
//
|
// Create control with a given color
|
//
|
BOOL CRectCtrl::Create(CWnd *pParentWnd, COLORREF c)
|
{
|
SetColor(c);
|
|
RECT r;
|
r.left = r.top = r.right = r.bottom = 0;
|
return CStatic::Create(0, WS_CHILD, r, pParentWnd);
|
}
|
|
BEGIN_MESSAGE_MAP(CRectCtrl, CStatic)
|
//{{AFX_MSG_MAP(CRectCtrl)
|
ON_WM_CTLCOLOR_REFLECT()
|
//}}AFX_MSG_MAP
|
END_MESSAGE_MAP()
|
|
/////////////////////////////////////////////////////////////////////////////
|
// CRectCtrl message handlers
|
|
HBRUSH CRectCtrl::CtlColor(CDC* /* pDC */, UINT /* nCtlColor */)
|
{
|
return (HBRUSH)m_Color.m_hObject;
|
}
|
|
/////////////////////////////////////////////////////////////////////////////
|
// CRectCtrl proporties
|
|
//
|
// Set the background color
|
//
|
void CRectCtrl::SetColor(COLORREF c)
|
{
|
m_Color.CreateSolidBrush(c);
|
}
|
|
/////////////////////////////////////////////////////////////////////////////
|
// Build in mouse cursors for zooming and scrolling support
|
//
|
|
static const int zoomInCursorA[] =
|
{
|
0xffffffff,
|
0xffffffff,
|
0xffffffff,
|
0xffffffff,
|
0xffffffff,
|
0xffffffff,
|
0xffffffff,
|
0xffffffff,
|
0xffffffff,
|
0xff3ff8ff,
|
0xff0fe0ff,
|
0xff07c0ff,
|
0xff07c0ff,
|
0xff0380ff,
|
0xff0380ff,
|
0xff0380ff,
|
0xff0380ff,
|
0xff0380ff,
|
0xff07c0ff,
|
0xff07c0ff,
|
0xff01e0ff,
|
0xff30f8ff,
|
0x7ff0ffff,
|
0x3ff8ffff,
|
0x1ffcffff,
|
0x0ffeffff,
|
0x0fffffff,
|
0x9fffffff,
|
0xffffffff,
|
0xffffffff,
|
0xffffffff,
|
0xffffffff
|
};
|
|
static const int zoomInCursorB[] =
|
{
|
0x00000000,
|
0x00000000,
|
0x00000000,
|
0x00000000,
|
0x00000000,
|
0x00000000,
|
0x00000000,
|
0x00000000,
|
0x00000000,
|
0x00000000,
|
0x00c00700,
|
0x00f01f00,
|
0x00f01e00,
|
0x00f83e00,
|
0x00f83e00,
|
0x00183000,
|
0x00f83e00,
|
0x00f83e00,
|
0x00f01e00,
|
0x00f01f00,
|
0x00c00700,
|
0x00000000,
|
0x00000000,
|
0x00000000,
|
0x00000000,
|
0x00000000,
|
0x00000000,
|
0x00000000,
|
0x00000000,
|
0x00000000,
|
0x00000000,
|
0x00000000
|
};
|
|
static const int zoomOutCursorA[] =
|
{
|
0xffffffff,
|
0xffffffff,
|
0xffffffff,
|
0xffffffff,
|
0xffffffff,
|
0xffffffff,
|
0xffffffff,
|
0xffffffff,
|
0xffffffff,
|
0xff3ff8ff,
|
0xff0fe0ff,
|
0xff07c0ff,
|
0xff07c0ff,
|
0xff0380ff,
|
0xff0380ff,
|
0xff0380ff,
|
0xff0380ff,
|
0xff0380ff,
|
0xff07c0ff,
|
0xff07c0ff,
|
0xff01e0ff,
|
0xff30f8ff,
|
0x7ff0ffff,
|
0x3ff8ffff,
|
0x1ffcffff,
|
0x0ffeffff,
|
0x0fffffff,
|
0x9fffffff,
|
0xffffffff,
|
0xffffffff,
|
0xffffffff,
|
0xffffffff
|
};
|
|
static const int zoomOutCursorB[] =
|
{
|
0x00000000,
|
0x00000000,
|
0x00000000,
|
0x00000000,
|
0x00000000,
|
0x00000000,
|
0x00000000,
|
0x00000000,
|
0x00000000,
|
0x00000000,
|
0x00c00700,
|
0x00f01f00,
|
0x00f01f00,
|
0x00f83f00,
|
0x00f83f00,
|
0x00183000,
|
0x00f83f00,
|
0x00f83f00,
|
0x00f01f00,
|
0x00f01f00,
|
0x00c00700,
|
0x00000000,
|
0x00000000,
|
0x00000000,
|
0x00000000,
|
0x00000000,
|
0x00000000,
|
0x00000000,
|
0x00000000,
|
0x00000000,
|
0x00000000,
|
0x00000000
|
};
|
|
static const int noZoomCursorA[] =
|
{
|
0xffffffff,
|
0xffffffff,
|
0xffffffff,
|
0xffffffff,
|
0xffffffff,
|
0xffffffff,
|
0xffffffff,
|
0xffffffff,
|
0xffffffff,
|
0xff3ff8ff,
|
0xff0fe0ff,
|
0xff07c0ff,
|
0xff07c0ff,
|
0xff0380ff,
|
0xff0380ff,
|
0xff0380ff,
|
0xff0380ff,
|
0xff0380ff,
|
0xff07c0ff,
|
0xff07c0ff,
|
0xff01e0ff,
|
0xff30f8ff,
|
0x7ff0ffff,
|
0x3ff8ffff,
|
0x1ffcffff,
|
0x0ffeffff,
|
0x0fffffff,
|
0x9fffffff,
|
0xffffffff,
|
0xffffffff,
|
0xffffffff,
|
0xffffffff
|
};
|
|
static const int noZoomCursorB[] =
|
{
|
0x00000000,
|
0x00000000,
|
0x00000000,
|
0x00000000,
|
0x00000000,
|
0x00000000,
|
0x00000000,
|
0x00000000,
|
0x00000000,
|
0x00000000,
|
0x00c00700,
|
0x00f01f00,
|
0x00f01f00,
|
0x00f83f00,
|
0x00f83f00,
|
0x00f83f00,
|
0x00f83f00,
|
0x00f83f00,
|
0x00f01f00,
|
0x00f01f00,
|
0x00c00700,
|
0x00000000,
|
0x00000000,
|
0x00000000,
|
0x00000000,
|
0x00000000,
|
0x00000000,
|
0x00000000,
|
0x00000000,
|
0x00000000,
|
0x00000000,
|
0x00000000
|
};
|
|
static int noMove2DCursorA[] =
|
{
|
0xffffffff,
|
0xffffffff,
|
0xffffffff,
|
0xff7ffeff,
|
0xff3ffcff,
|
0xff1ff8ff,
|
0xff0ff0ff,
|
0xff07e0ff,
|
0xff03c0ff,
|
0xff03c0ff,
|
0xfffc3fff,
|
0x7ffc3ffe,
|
0x3ffc3ffc,
|
0x1f7c3ef8,
|
0x0f3c3cf0,
|
0x071c38e0,
|
0x071c38e0,
|
0x0f3c3cf0,
|
0x1f7c3ef8,
|
0x3ffc3ffc,
|
0x7ffc3ffe,
|
0xfffc3fff,
|
0xff03c0ff,
|
0xff03c0ff,
|
0xff07e0ff,
|
0xff0ff0ff,
|
0xff1ff8ff,
|
0xff3ffcff,
|
0xff7ffeff,
|
0xffffffff,
|
0xffffffff,
|
0xffffffff
|
};
|
|
static int noMove2DCursorB[] =
|
{
|
0x00000000,
|
0x00000000,
|
0x00000000,
|
0x00800100,
|
0x00400200,
|
0x00200400,
|
0x00100800,
|
0x00081000,
|
0x00042000,
|
0x00fc3f00,
|
0x0003c000,
|
0x80024001,
|
0x40024002,
|
0x20824104,
|
0x10424208,
|
0x08224410,
|
0x08224410,
|
0x10424208,
|
0x20824104,
|
0x40024002,
|
0x80024001,
|
0x0003c000,
|
0x00fc3f00,
|
0x00042000,
|
0x00081000,
|
0x00100800,
|
0x00200400,
|
0x00400200,
|
0x00800100,
|
0x00000000,
|
0x00000000,
|
0x00000000
|
};
|
|
static int noMoveHorizCursorA[] =
|
{
|
0xffffffff,
|
0xffffffff,
|
0xffffffff,
|
0xffffffff,
|
0xffffffff,
|
0xffffffff,
|
0xffffffff,
|
0xffffffff,
|
0xffffffff,
|
0xffffffff,
|
0xfffc3fff,
|
0x7ffc3ffe,
|
0x3ffc3ffc,
|
0x1f7c3ef8,
|
0x0f3c3cf0,
|
0x071c38e0,
|
0x071c38e0,
|
0x0f3c3cf0,
|
0x1f7c3ef8,
|
0x3ffc3ffc,
|
0x7ffc3ffe,
|
0xfffc3fff,
|
0xffffffff,
|
0xffffffff,
|
0xffffffff,
|
0xffffffff,
|
0xffffffff,
|
0xffffffff,
|
0xffffffff,
|
0xffffffff,
|
0xffffffff,
|
0xffffffff
|
};
|
|
static int noMoveHorizCursorB[] =
|
{
|
0x00000000,
|
0x00000000,
|
0x00000000,
|
0x00000000,
|
0x00000000,
|
0x00000000,
|
0x00000000,
|
0x00000000,
|
0x00000000,
|
0x00000000,
|
0x0003c000,
|
0x80024001,
|
0x40024002,
|
0x20824104,
|
0x10424208,
|
0x08224410,
|
0x08224410,
|
0x10424208,
|
0x20824104,
|
0x40024002,
|
0x80024001,
|
0x0003c000,
|
0x00000000,
|
0x00000000,
|
0x00000000,
|
0x00000000,
|
0x00000000,
|
0x00000000,
|
0x00000000,
|
0x00000000,
|
0x00000000,
|
0x00000000
|
};
|
|
static int noMoveVertCursorA[] =
|
{
|
0xffffffff,
|
0xffffffff,
|
0xffffffff,
|
0xff7ffeff,
|
0xff3ffcff,
|
0xff1ff8ff,
|
0xff0ff0ff,
|
0xff07e0ff,
|
0xff03c0ff,
|
0xff03c0ff,
|
0xffffffff,
|
0xffffffff,
|
0xffffffff,
|
0xff7ffeff,
|
0xff3ffcff,
|
0xff1ff8ff,
|
0xff1ff8ff,
|
0xff3ffcff,
|
0xff7ffeff,
|
0xffffffff,
|
0xffffffff,
|
0xffffffff,
|
0xff03c0ff,
|
0xff03c0ff,
|
0xff07e0ff,
|
0xff0ff0ff,
|
0xff1ff8ff,
|
0xff3ffcff,
|
0xff7ffeff,
|
0xffffffff,
|
0xffffffff,
|
0xffffffff
|
};
|
|
static int noMoveVertCursorB[] =
|
{
|
0x00000000,
|
0x00000000,
|
0x00000000,
|
0x00800100,
|
0x00400200,
|
0x00200400,
|
0x00100800,
|
0x00081000,
|
0x00042000,
|
0x00fc3f00,
|
0x00000000,
|
0x00000000,
|
0x00000000,
|
0x00800100,
|
0x00400200,
|
0x00200400,
|
0x00200400,
|
0x00400200,
|
0x00800100,
|
0x00000000,
|
0x00000000,
|
0x00000000,
|
0x00fc3f00,
|
0x00042000,
|
0x00081000,
|
0x00100800,
|
0x00200400,
|
0x00400200,
|
0x00800100,
|
0x00000000,
|
0x00000000,
|
0x00000000
|
};
|
|
static HCURSOR hZoomInCursor = 0;
|
static HCURSOR hZoomOutCursor = 0;
|
static HCURSOR hNoZoomCursor = 0;
|
static HCURSOR hNoMove2DCursor = 0;
|
static HCURSOR hNoMoveHorizCursor = 0;
|
static HCURSOR hNoMoveVertCursor = 0;
|
|
class FreeCursors
|
{
|
public:
|
~FreeCursors()
|
{
|
if (0 != hZoomInCursor)
|
DestroyCursor(hZoomInCursor);
|
if (0 != hZoomOutCursor)
|
DestroyCursor(hZoomOutCursor);
|
if (0 != hNoZoomCursor)
|
DestroyCursor(hNoZoomCursor);
|
if (0 != hNoMove2DCursor)
|
DestroyCursor(hNoMove2DCursor);
|
if (0 != hNoMoveHorizCursor)
|
DestroyCursor(hNoMoveHorizCursor);
|
if (0 != hNoMoveVertCursor)
|
DestroyCursor(hNoMoveVertCursor);
|
}
|
} dummyFreeCursorObj;
|
|
static HCURSOR getZoomInCursor()
|
{
|
if (0 == hZoomInCursor)
|
{
|
hZoomInCursor = CreateCursor(AfxGetApp()->m_hInstance, 15, 15, 32, 32,
|
zoomInCursorA, zoomInCursorB);
|
}
|
return hZoomInCursor;
|
}
|
|
static HCURSOR getZoomOutCursor()
|
{
|
if (0 == hZoomOutCursor)
|
{
|
hZoomOutCursor = CreateCursor(AfxGetApp()->m_hInstance, 15, 15, 32, 32,
|
zoomOutCursorA, zoomOutCursorB);
|
}
|
return hZoomOutCursor;
|
}
|
|
static HCURSOR getNoZoomCursor()
|
{
|
if (0 == hNoZoomCursor)
|
{
|
hNoZoomCursor = CreateCursor(AfxGetApp()->m_hInstance, 15, 15, 32, 32,
|
noZoomCursorA, noZoomCursorB);
|
}
|
return hNoZoomCursor;
|
}
|
|
static HCURSOR getNoMove2DCursor()
|
{
|
if (0 == hNoMove2DCursor)
|
{
|
hNoMove2DCursor = CreateCursor(AfxGetApp()->m_hInstance, 15, 15, 32, 32,
|
noMove2DCursorA, noMove2DCursorB);
|
}
|
return hNoMove2DCursor;
|
}
|
|
static HCURSOR getNoMoveHorizCursor()
|
{
|
if (0 == hNoMoveHorizCursor)
|
{
|
hNoMoveHorizCursor = CreateCursor(AfxGetApp()->m_hInstance, 15, 15, 32, 32,
|
noMoveHorizCursorA, noMoveHorizCursorB);
|
}
|
return hNoMoveHorizCursor;
|
}
|
|
static HCURSOR getNoMoveVertCursor()
|
{
|
if (0 == hNoMoveVertCursor)
|
{
|
hNoMoveVertCursor = CreateCursor(AfxGetApp()->m_hInstance, 15, 15, 32, 32,
|
noMoveVertCursorA, noMoveVertCursorB);
|
}
|
return hNoMoveVertCursor;
|
}
|