suppose that I have COpenGLControl class downloaded here from codeguru assuming that the first event handler runned when creating the OpenGL window is OnCreate, I have tried to catch up the errors of this class. here's the code used to create the window in my dialog's .h and .cpp files:
MyOpenGLTestDlg.h
COpenGLControl m_oglWindow;
MyOpenGLTestDlg.cpp
CRect rect;
// Get size and position of the picture control
GetDlgItem(ID_OPENGL)->GetWindowRect(rect);
// Convert screen coordinates to client coordinates
ScreenToClient(rect);
up to know I think the function OnCreate has been invoked. In fact, I think the line of code COpenGLControl m_oglWindow; causes this function to be invoked! but I'm not sure so it will be appreciated if you guide me alittle about this?
anyway, I have not made a lot of change to the class:
OpenGLControl.h
#pragma once
#include "afxwin.h"
#include "WinBase.h"
#include <gl/gl.h>
#include <gl/glu.h>
class COpenGLControl : public CWnd
{
public:
/******************/
/* Public Members */
/******************/
UINT_PTR m_unpTimer;
// View information variables
float m_fLastX;
float m_fLastY;
float m_fPosX;
float m_fPosY;
float m_fZoom;
float m_fRotX;
float m_fRotY;
bool m_bIsMaximized;
private:
/*******************/
/* Private Members */
/*******************/
// Window information
CWnd *hWnd; //window handle
HDC hdc; //device context handle
HGLRC hrc; //handle to GL Rendering Context
int m_nPixelFormat;
CRect m_rect;
CRect m_oldWindow;
CRect m_originalRect;
public:
COpenGLControl(void);
virtual ~COpenGLControl(void);
void oglCreate(CRect rect, CWnd *parent);
void oglInitialize(void);
void oglDrawScene(void);
// Added message classes:
afx_msg void OnPaint();
afx_msg void OnSize(UINT nType, int cx, int cy);
afx_msg void OnDraw(CDC *pDC);
afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct);
afx_msg void OnTimer(UINT nIDEvent);
afx_msg void OnMouseMove(UINT nFlags, CPoint point);
DECLARE_MESSAGE_MAP()
};
OpenGLControl.cpp
int COpenGLControl::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
if (CWnd::OnCreate(lpCreateStruct) == -1) return -1;
oglInitialize();
return 0;
}
void COpenGLControl::oglInitialize(void)
{
// Initial Setup:
//
static PIXELFORMATDESCRIPTOR pfd =
{
sizeof(PIXELFORMATDESCRIPTOR),
1,
PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER,
PFD_TYPE_RGBA,
32, // bit depth
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
24, // z-buffer depth
8,0,PFD_MAIN_PLANE, 0, 0, 0, 0,
};
// Get device context only once.
hdc = GetDC()->m_hDC;
// Pixel format.
m_nPixelFormat = ChoosePixelFormat(hdc, &pfd);
SetPixelFormat(hdc, m_nPixelFormat, &pfd);
// Create the OpenGL Rendering Context.
hrc = wglCreateContext(hdc);
GLenum error13 = glGetError();
wglMakeCurrent(hdc, hrc);
// Basic Setup:
//
// Set color to use when clearing the background.
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClearDepth(1.0f);
// Turn on backface culling
glFrontFace(GL_CCW);
glCullFace(GL_BACK);
// Turn on depth testing
glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_LEQUAL);
// Send draw request
OnDraw(NULL);
}
As you see I have written the code GLenum error13 = glGetError(); right after hrc = wglCreateContext(hdc); to catch any probable error that it throws and yes the value for error13 is 1282 which means INVALID_OPERATION so I think the handle for OpenGL rendering context is not created properly!
besides if you check the values for hdc and hrc, you encounter these:
hdc -> unused = ??? (Error: expression can not be evaluated)
hrc -> unused = 0
Could you help me find out why it is the case? and what's the problem?
The results are quite undefined if you call GL functions (like glGetError() itself) without having a current GL context.
wglCreateContext() is part of the Win33 API (and not the GL API) and will signal errors by returning a NULL pointer. You can call the Windows API function GetLastError() if you want details in that case, like with most other Windows API functions.
Do not call an OpenGL API function before you have a "current context" for the calling thread in Win32. The operations will be undefined.
To correct this issue, simply move the call to wglMakeCurrent (...) one line up so that it comes before the call to glGetError (...).
Note that you have to do this for every thread, and in Win32 only one thread is allowed to access an OpenGL context at any given time. If you ever want to do multi-threaded rendering in Win32, you will have to either acquire/release the context and do synchronization between threads or use a bunch of contexts that "share lists" (wglShareLists (...)).
Like derhass mentioned, if you want information about an error generated by the WGL API, use GetLastError (...). WGL is part of the window system, which is built on top of the Win32 API, so it will communicate its errors to you through the traditional Win32 channels.
By the way, try not to print GL error values in decimal form. They are always enumerated in gl.h as hexadecimal constants, so it would be easier to find the appropriate enumerant if you did this:
printf ("OpenGL Error: 0x%X\n", err);
The same goes for all enumerated constants in OpenGL. If you do not have a function that will map them to a human-readable string, you should use the hexadecimal value to look them up.
Related
I'm updating the question to remove irrelevant details. The conclusion I've drawn is that if a valid alpha channel exists, it will honor it, but if it doesn't (say a 24-bit PNG w/o alpha channel), it uses F0F0F0 as a transparent color.
I have an image being loaded into a static "picture control" (chosen in visual studio) in a dialog. I noticed that color 0xF0F0F0 is being displayed as a "transparent" color (background of the dialog bleeds through). The bitmap is loaded via CStatic::SetBitmap.
The Picture Control transparent flag is set to false.
The image is loaded via CImage::Load.
If I wanted to mask a color out of a CStatic bitmap set via SetBitmap, how would I do it? I don't, but maybe that would help me find the cause.
Minimum example below. I created a dialog project with the VS wizard, and added a picture control to the main dialog. Then I added only the following code:
//header code added
CPngImage logoImage;
CStatic pictureCtrl;
CBrush bgBrush;
....
afx_msg HBRUSH OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor);
//cpp code added
DDX_Control(pDX, IDC_STATICIMG, pictureCtrl);
....
ON_WM_CTLCOLOR()
....
bgBrush.CreateSolidBrush(RGB(0, 255, 0));
logoImage.LoadFromFile(_T("C:\\temp\\logo.png"));
pictureCtrl.SetBitmap(logoImage);
....
HBRUSH CMFCApplication1Dlg::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor) {
return bgBrush;
}
And here is the image file I'm testing with.
And here is what it looks like on the dialog:
// MFCApplication1Dlg.h : header file
//
#pragma once
// CMFCApplication1Dlg dialog
class CMFCApplication1Dlg : public CDialogEx
{
// Construction
public:
CMFCApplication1Dlg(CWnd* pParent = nullptr); // standard constructor
CPngImage logoImage;
CStatic pictureCtrl;
CBrush bgBrush;
// Dialog Data
#ifdef AFX_DESIGN_TIME
enum { IDD = IDD_MFCAPPLICATION1_DIALOG };
#endif
protected:
virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV support
// Implementation
protected:
HICON m_hIcon;
// Generated message map functions
virtual BOOL OnInitDialog();
afx_msg void OnSysCommand(UINT nID, LPARAM lParam);
afx_msg void OnPaint();
afx_msg HCURSOR OnQueryDragIcon();
DECLARE_MESSAGE_MAP()
public:
afx_msg HBRUSH OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor);
};
// MFCApplication1Dlg.cpp : implementation file
//
#include "stdafx.h"
#include "MFCApplication1.h"
#include "MFCApplication1Dlg.h"
#include "afxdialogex.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#endif
// CAboutDlg dialog used for App About
class CAboutDlg : public CDialogEx
{
public:
CAboutDlg();
// Dialog Data
#ifdef AFX_DESIGN_TIME
enum { IDD = IDD_ABOUTBOX };
#endif
protected:
virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV support
// Implementation
protected:
DECLARE_MESSAGE_MAP()
};
CAboutDlg::CAboutDlg() : CDialogEx(IDD_ABOUTBOX)
{
}
void CAboutDlg::DoDataExchange(CDataExchange* pDX)
{
CDialogEx::DoDataExchange(pDX);
}
BEGIN_MESSAGE_MAP(CAboutDlg, CDialogEx)
END_MESSAGE_MAP()
// CMFCApplication1Dlg dialog
CMFCApplication1Dlg::CMFCApplication1Dlg(CWnd* pParent /*=nullptr*/)
: CDialogEx(IDD_MFCAPPLICATION1_DIALOG, pParent)
{
m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME);
}
void CMFCApplication1Dlg::DoDataExchange(CDataExchange* pDX)
{
CDialogEx::DoDataExchange(pDX);
DDX_Control(pDX, IDC_STATICIMG, pictureCtrl);
}
BEGIN_MESSAGE_MAP(CMFCApplication1Dlg, CDialogEx)
ON_WM_SYSCOMMAND()
ON_WM_PAINT()
ON_WM_QUERYDRAGICON()
ON_WM_CTLCOLOR()
END_MESSAGE_MAP()
// CMFCApplication1Dlg message handlers
BOOL CMFCApplication1Dlg::OnInitDialog()
{
CDialogEx::OnInitDialog();
// Add "About..." menu item to system menu.
// IDM_ABOUTBOX must be in the system command range.
ASSERT((IDM_ABOUTBOX & 0xFFF0) == IDM_ABOUTBOX);
ASSERT(IDM_ABOUTBOX < 0xF000);
CMenu* pSysMenu = GetSystemMenu(FALSE);
if (pSysMenu != nullptr)
{
BOOL bNameValid;
CString strAboutMenu;
bNameValid = strAboutMenu.LoadString(IDS_ABOUTBOX);
ASSERT(bNameValid);
if (!strAboutMenu.IsEmpty())
{
pSysMenu->AppendMenu(MF_SEPARATOR);
pSysMenu->AppendMenu(MF_STRING, IDM_ABOUTBOX, strAboutMenu);
}
}
// Set the icon for this dialog. The framework does this automatically
// when the application's main window is not a dialog
SetIcon(m_hIcon, TRUE); // Set big icon
SetIcon(m_hIcon, FALSE); // Set small icon
bgBrush.CreateSolidBrush(RGB(0, 255, 0));
logoImage.LoadFromFile(_T("C:\\temp\\logo.png"));
pictureCtrl.SetBitmap(logoImage);
return TRUE; // return TRUE unless you set the focus to a control
}
void CMFCApplication1Dlg::OnSysCommand(UINT nID, LPARAM lParam)
{
if ((nID & 0xFFF0) == IDM_ABOUTBOX)
{
CAboutDlg dlgAbout;
dlgAbout.DoModal();
}
else
{
CDialogEx::OnSysCommand(nID, lParam);
}
}
// If you add a minimize button to your dialog, you will need the code below
// to draw the icon. For MFC applications using the document/view model,
// this is automatically done for you by the framework.
void CMFCApplication1Dlg::OnPaint()
{
if (IsIconic())
{
CPaintDC dc(this); // device context for painting
SendMessage(WM_ICONERASEBKGND, reinterpret_cast<WPARAM>(dc.GetSafeHdc()), 0);
// Center icon in client rectangle
int cxIcon = GetSystemMetrics(SM_CXICON);
int cyIcon = GetSystemMetrics(SM_CYICON);
CRect rect;
GetClientRect(&rect);
int x = (rect.Width() - cxIcon + 1) / 2;
int y = (rect.Height() - cyIcon + 1) / 2;
// Draw the icon
dc.DrawIcon(x, y, m_hIcon);
}
else
{
CDialogEx::OnPaint();
}
}
// The system calls this function to obtain the cursor to display while the user drags
// the minimized window.
HCURSOR CMFCApplication1Dlg::OnQueryDragIcon()
{
return static_cast<HCURSOR>(m_hIcon);
}
HBRUSH CMFCApplication1Dlg::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor) {
return bgBrush;
}
On my system (Windows 10), the color 0xF0F0F0 equals GetSysColor(COLOR_BTNFACE) which is the default dialog background color. When drawing, the static control seems to replace this color in the background image with the brush returned from OnCtlColor() handler of the parent window. This does have the taste of a feature and not a bug (though I couldn't find anything in the reference that specifies this behaviour).
Here is a code snippet to reproduce this issue even without using CPngImage or CImage, just by drawing in a memory DC with color 0xF0F0F0.
As the behaviour only appears when the source image does not contain an alpha channel, a solution would be to convert the source image to 32-bpp ARGB format. This way we don't have to override CStatic::OnPaint():
// Set the alpha channel of a 32-bpp ARGB image to the given value.
HRESULT SetAlphaChannel( CImage& image, std::uint8_t alpha )
{
if( ! image.GetBits() || image.GetBPP() != 32 )
return E_INVALIDARG;
GdiFlush(); // Make sure GDI has finished all drawing in source image.
for( int y = 0; y < image.GetHeight(); ++y )
{
DWORD* pPix = reinterpret_cast<DWORD*>( image.GetPixelAddress( 0, y ) );
for( int x = 0; x < image.GetWidth(); ++x, ++pPix )
{
*pPix = ( *pPix & 0xFFFFFF ) | ( alpha << 24 );
}
}
return S_OK;
}
// Load an image and convert to 32-bpp ARGB format, if necessary.
HRESULT LoadImageAndConvertToARGB32( CImage& image, LPCWSTR pFilePath )
{
CImage tempImage;
HRESULT hr = tempImage.Load( pFilePath );
if( FAILED( hr ) )
return hr;
if( tempImage.GetBPP() == 32 ) // Assume 32 bpp image already has an alpha channel
{
image.Attach( tempImage.Detach() );
return S_OK;
}
if( ! image.Create( tempImage.GetWidth(), tempImage.GetHeight(), 32, CImage::createAlphaChannel ) )
return E_FAIL;
HDC const imageDC = image.GetDC();
BOOL const bitBltSuccess = tempImage.BitBlt( imageDC, 0, 0, SRCCOPY );
image.ReleaseDC();
if( ! bitBltSuccess )
return E_FAIL;
SetAlphaChannel( image, 255 ); // set alpha to opaque
return S_OK;
}
Usage:
Replace call to CImage::Load() by:
LoadImageAndConvertToARGB32( m_image, filePath );
Notes:
There is another static control nastiness when you assign a 32-bpp bitmap with a non-zero alpha channel to the control¹ (as you do when following my solution). In this case, the static control will make a copy of the bitmap you passed in while you are responsible to destroy this copy!
Mandatory OldNewThing read:
"When will the static control automatically delete the image loaded into it, and when is it the responsibility of the application?"
¹) More precisely: When using version 6 of the common controls, which almost all applications do these days.
It's not a PNG problem, it's a color depth problem.
According to your code, I converted 8-bit PNG picture into 8-bit BMP picture by format conversion tool, and the picture still show the background color.
So I saved 8-bit PNG picture to 32-bit png picture, and that's all right.
Why is that?
Answer: A GIF file has two parts: a color table and the image pixel data. The color table is a list of the colors used in that image (an 8-bit GIF can have up to 2^8 = 256 colors in the color table, but a 4-bit GIF can have only 2^4 = 16 colors), and each color is assigned a number. The image pixel data are for the image itself, and each pixel is assigned a number that points to its color in the color table. For example, if color #10 in the color table is red (#FF0000), then any pixel in the image with the number 10 will be displayed as red. The colors in the color table will vary from GIF file to GIF file based on the image itself; color #10 will not always be red. The color table is the set of up to 256 colors necessary to render that image.
When we add index transparency, every color in the color table is given a transparency designation in addition to its color data (i.e., RGB values):
zero (o = False in Boolean algebra) means do not display this color, or
one (1 = True in Boolean Algebra) means display this color.
There are no intermediate opacities; the color is either displayed or it is not. The end result is that a pixel with an index transparency color will not be displayed and whatever is in the background behind that pixel will show through. For example, if color #10 is red (#FF0000) and is designated as transparent (index transparency = 0), then any pixel that is color #10 will not be displayed and the background will show through.
There can be multiple transparent colors in index transparency, because every color in the color table has a designation of either opaque (1) or transparent (0). Most graphics programs assume that the canvas color (often white, but it could be any color) is the default transparent color, but you can specify any color (or any number of colors) as transparent or not.
This type of transparency is very common in GIF and PNG8 files and is easy to identify, because there is no fading, there are no partially transparent pixels, and the edges are often described as “hard” or “pixelated.”
I'm afraid the following is the best answer I'm going to get. MFC/Win32 does something funny and it's not clear why. But the image has no problems displaying correctly if you draw it manually.
I created a custom (very rough) CStatic class and added the OnPaint below. In the screenshot, the first is using my class, and the second is using regular CStatic. Both are using an identical image (24 bit BMP).
struct AFX_CTLCOLOR {
HWND hWnd;
HDC hDC;
UINT nCtlType;
};
void CMyStatic::OnPaint() {
CPaintDC dc(this); // device context for painting
CRect r;
GetClientRect(r);
WPARAM w = (WPARAM)&dc;
AFX_CTLCOLOR ctl;
ctl.hWnd = this->GetSafeHwnd();
ctl.nCtlType = CTLCOLOR_STATIC;
ctl.hDC = dc.GetSafeHdc();
HBRUSH bg=(HBRUSH)::SendMessage(GetParent()->GetSafeHwnd(), WM_CTLCOLORSTATIC, w, (LPARAM)&ctl);
CBrush cbg;
cbg.Attach(bg);
dc.FillRect(r, &cbg);
cbg.Detach();
HBITMAP hbmp=GetBitmap();
BITMAP binfo;
CBitmap* cbmp = CBitmap::FromHandle(hbmp);
cbmp->GetBitmap(&binfo);
CDC dcMem;
dcMem.CreateCompatibleDC(&dc);
dcMem.SelectObject(cbmp);
dc.BitBlt((r.Width()-binfo.bmWidth)/2, (r.Height() - binfo.bmHeight) / 2, binfo.bmWidth, binfo.bmHeight, &dcMem, 0, 0, SRCCOPY);
}
I am creating a simple Win32/MFC application with a main window, and a child-window that uses AnimateWindow() to show (slide up) and hide (slide down) the window. When running the application on 100% dpi scaling, everything behaves normally.
I have overridden WM_ERASEBKGND to render a random red color, to demonstrate the effect. As the window slides-down (hide) on every "step" of the animation the "update rectangle" of the background is repainted, exactly where the child-window disappeared, and the background is to become visible again.
However, when changing the dpi-scaling via Windows Settings (in this case to 125%), an incorrect area is redrawn. It appears as if the area passed into OnEraseBkgnd() is still using the 100% dpi-scaling size, but also the position is off: it appears as if the x/y position of the upper-left corner is passed in screen-space instead of client-space. So the redrawn area looks different, depending on where the on the screen the window is positioned.
The white area is where the child window was actually positioned, and where the redraw of the background should actually have happened.
I have confirmed this effect on Win10 (1803 and 1809) and on Win8.1
Is this a bug in the OS, or is there something I can do to avoid the problem - other than not using AnimateWindow()? ShowWindow() (with SW_SHOW or SW_HIDE) works just fine, btw.
Update: added the full source-code to reproduce the issue.
The problem occurs when using no dpi-aware manifest at all, but it also occurs when using <gdiScaling xmlns="http://schemas.microsoft.com/SMI/2017/WindowsSettings">true</gdiScaling>
class CDialogTestApp : public CWinApp
{
virtual BOOL InitInstance();
};
CDialogTestApp theApp;
class CAboutDlg : public CDialogEx
{
public:
CAboutDlg() : CDialogEx(IDD_ABOUTBOX) {}
};
class CDialogTestDlg : public CDialogEx
{
public:
CDialogTestDlg(CWnd* pParent = nullptr) : CDialogEx(IDD_DIALOGTEST_DIALOG, pParent) {}
protected:
virtual BOOL OnInitDialog();
afx_msg BOOL OnEraseBkgnd(CDC* pDC);
afx_msg void OnLButtonUp(UINT nFlags, CPoint point);
DECLARE_MESSAGE_MAP()
private:
CAboutDlg mDialog;
};
BEGIN_MESSAGE_MAP(CDialogTestDlg, CDialogEx)
ON_WM_ERASEBKGND()
ON_WM_LBUTTONUP()
END_MESSAGE_MAP()
BOOL CDialogTestApp::InitInstance()
{
CWinApp::InitInstance();
CDialogTestDlg dlg;
m_pMainWnd = &dlg;
INT_PTR nResponse = dlg.DoModal();
return FALSE;
}
BOOL CDialogTestDlg::OnInitDialog()
{
CDialogEx::OnInitDialog();
mDialog.Create(IDD_ABOUTBOX, this);
return TRUE;
}
BOOL CDialogTestDlg::OnEraseBkgnd(CDC* pDC)
{
COLORREF color = RGB(rand() & 255, 20, 40);
CRect rect;
GetClipBox(pDC->m_hDC, &rect); // retrieve the update-rectangle
CBrush brush(color);
FillRect(pDC->m_hDC, &rect, (HBRUSH)brush.m_hObject);
return TRUE;
}
void CDialogTestDlg::OnLButtonUp(UINT nFlags, CPoint point)
{
if (mDialog.IsWindowVisible())
{
mDialog.AnimateWindow(200, AW_HIDE | AW_SLIDE | AW_VER_POSITIVE);
}
else
{
mDialog.SetWindowPos(&CWnd::wndTop, 0, 50, 0, 0, SWP_NOSIZE);
mDialog.AnimateWindow(200, AW_ACTIVATE | AW_SLIDE | AW_VER_NEGATIVE);
}
CDialogEx::OnLButtonUp(nFlags, point);
}
I am working on a screen capture program using C++11, MinGW, and the Windows API. I am trying to use SDL2 to watch how my screen capture program works in real time.
The window opens fine, and the program seems to run well so long as I do nothing more than move the mouse cursor. But iff I click in the window, its menu bar, outside the window, or press any keys, the SDL window freezes.
I have set up some logging for the events to figure out what is happening. I never receive any events other than SDL_WINDOW_FOCUS_GAINED, SDL_TEXTEDITING, and SDL_WINDOWEVENT_SHOWN in that order. All of these are received at the start.
I have tried to find tutorials on SDL event handling since that's my best guess as to the source of the problem. I have found nothing more than basic event handling to watch for SDL_QUIT, basic mouse and keyboard events, and one on SDL_WINDOWEVENTs that does not seem to help. I have found nothing in-depth on what the events mean and best practices for handling them. That may not matter, because that might not be the source of the problem. For all I know, SDL is throwing a fit because there are other threads running.
Can anyone see any cause for this hanging in my code and provide an explanation as to how to fix it?
A quick explanation for the structure of my program is in order to cover the code I have omitted. The Captor class starts and runs a thread to grab a screenshot to pass to the Encoder. The Encoder starts a variable number of threads that receive a screenshot from the Captor, encode the screenshot, then passes the encoding to the Screen. The passing mechanism is the SynchronousQueue<T> class that provides paired methods put(const T&) and T get() to allow a producer and a consumer to synchronize using a resource; these methods time out to allow the the system to be responsive to kill messages.
Now for the source files (hopefully without too much bloat). While I would appreciate any comments on how to improve the performance of the application, my focus is on making the program responsive.
main.cpp
#include "RTSC.hpp"
int main(int argc, char** argv) {
RTSC rtsc {
(uint32_t) stoi(argv[1]),
(uint32_t) stoi(argv[2]),
(uint32_t) stoi(argv[3]),
(uint32_t) stoi(argv[4]),
(uint32_t) stoi(argv[5]),
(uint32_t) stoi(argv[6])
};
while (rtsc.isRunning()) {
SwitchToThread();
}
return 0;
}
RTSC.hpp
#ifndef RTSC_HPP
#define RTSC_HPP
#include "Captor.hpp"
#include "Encoder.hpp"
#include "Screen.hpp"
#include <iostream>
using namespace std;
class RTSC {
private:
Captor *captor;
Encoder *encoder;
SynchronousQueue<uint8_t*> imageQueue {1};
SynchronousQueue<RegionList> regionQueue {1};
Screen *screen;
public:
RTSC(
uint32_t width,
uint32_t height,
uint32_t maxRegionCount,
uint32_t threadCount,
uint32_t divisionsAlongThreadWidth,
uint32_t divisionsAlongThreadHeight
) {
captor = new Captor(width, height, imageQueue);
encoder = new Encoder(
width,
height,
maxRegionCount,
threadCount,
divisionsAlongThreadWidth,
divisionsAlongThreadHeight,
imageQueue,
regionQueue
);
screen = new Screen(
width,
height,
width >> 1,
height >> 1,
regionQueue
);
captor->start();
}
~RTSC() {
delete screen;
delete encoder;
delete captor;
}
bool isRunning() const {
return screen->isRunning();
}
};
#endif
Screen.hpp
#ifndef SCREEN_HPP
#define SCREEN_HPP
#include <atomic>
#include <SDL.h>
#include <windows.h>
#include "Region.hpp"
#include "SynchronousQueue.hpp"
using namespace std;
class Screen {
private:
atomic_bool running {false};
HANDLE thread;
SynchronousQueue<RegionList>* inputQueue;
uint32_t inputHeight;
uint32_t inputWidth;
uint32_t screenHeight;
uint32_t screenWidth;
SDL_Renderer* renderer;
SDL_Surface* surface;
SDL_Texture* texture;
SDL_Window* window;
void run() {
SDL_Event event;
while (running) {
while (SDL_PollEvent(&event)) {
switch (event.type) {
case SDL_QUIT:
running = false;
break;
case SDL_WINDOWEVENT:
switch (event.window.event) {
case SDL_WINDOWEVENT_CLOSE:
running = false;
break;
default:
break;
}
}
try {
RegionList rl = inputQueue->get();
SDL_RenderClear(renderer);
SDL_LockSurface(surface);
SDL_FillRect(surface, nullptr, 0);
for (uint32_t i = 0; i < rl.count; ++i) {
Region &r = rl.regions[i];
SDL_Rect rect {
(int) r.getX(),
(int) r.getY(),
(int) r.getWidth(),
(int) r.getHeight()
};
uint32_t color =
(r.getRed() << 16) +
(r.getGreen() << 8) +
r.getBlue();
SDL_FillRect(surface, &rect, color);
}
SDL_UnlockSurface(surface);
SDL_UpdateTexture(
texture,
nullptr,
surface->pixels,
surface->pitch
);
SDL_RenderCopyEx(
renderer,
texture,
nullptr,
nullptr,
0,
nullptr,
SDL_FLIP_VERTICAL
);
} catch (exception &e) {}
SDL_RenderPresent(renderer);
SwitchToThread();
}
}
static DWORD startThread(LPVOID self) {
((Screen*) self)->run();
return (DWORD) 0;
}
public:
Screen(
uint32_t inputWidth,
uint32_t inputHeight,
uint32_t windowWidth,
uint32_t windowHeight,
SynchronousQueue<RegionList> &inputQueue
): inputQueue {&inputQueue}, inputHeight {inputHeight} {
SDL_Init(SDL_INIT_VIDEO);
window = SDL_CreateWindow(
"RTSC",
SDL_WINDOWPOS_CENTERED,
SDL_WINDOWPOS_CENTERED,
windowWidth,
windowHeight,
SDL_WINDOW_SHOWN | SDL_WINDOW_RESIZABLE |
SDL_WINDOW_INPUT_FOCUS | SDL_WINDOW_MOUSE_FOCUS
);
renderer = SDL_CreateRenderer(window, -1, 0);
surface = SDL_CreateRGBSurface(
0,
inputWidth,
inputHeight,
24,
0xFF << 16,
0xFF << 8,
0xFF,
0
);
texture = SDL_CreateTexture(
renderer,
surface->format->format,
SDL_TEXTUREACCESS_STREAMING,
inputWidth,
inputHeight
);
running = true;
thread = CreateThread(nullptr, 0, startThread, this, 0, nullptr);
}
~Screen() {
running = false;
WaitForSingleObject(thread, INFINITE);
CloseHandle(thread);
SDL_FreeSurface(surface);
SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(window);
SDL_Quit();
}
bool isRunning() const {
return running;
}
};
#endif
I have no experience in using SDL API in a multithreaded environment but this isn't a big problem as you will see later. I've checked your code and there is at least one thing you should change in my opinion.
Generally, in case of GUI systems (and partly SDL is also a gui system) you should always access the gui only from the main thread and expect the gui events to come from the main thread. Most GUI APIs are single threaded and I wouldn't be surprised if this would apply to SDL too. Note that many gui systems run on the main thread of your process by default and you can't choose your own thread. Don't run the code of your Screen class on a worker thread, run it on your main thread and make EVERY SDL API call from the main thread.
If you are writing the game or a similar software then (first) write it as if it was single threaded. The subsystems of your engine (physics simulation, this-and-that-system, game logic, rendering) should be executed serially one-after-the-other on your main thread (from your main loop). If you want to make use of multithreading that do that in "another dimension": Convert some of the subsystems or a smaller unit of work (like merge sort) to multithreaded, for example a physics system tasks can often be split into several small tasks so when the physics system is updated by the main thread then the physics system can burn all of your cores...
Doing most of your tasks on the main thread has another advantage: It makes your code much more easy to port to any platform. Optionally if you write your code so that it can execute in single threaded mode then it can make debugging easier in many cases and then you also have a "reference build" to compare the multithreaded build to performancewise.
I am trying to implement an owner draw CSliderCtrl. It seems drawing is fine, however the mouse to change the thumb position is very weird. It is limited only in a very small rect. And I found that the rect by GetChannelRect is much smaller than the rect I used to create the slider control.
Below is .h file:
#if !defined(AFX_OWNDRAWSLIDER_H__82981708_4CBC_4D1E_8982_504E99BE489D__INCLUDED_)
#define AFX_OWNDRAWSLIDER_H__82981708_4CBC_4D1E_8982_504E99BE489D__INCLUDED_
#if _MSC_VER > 1000
#pragma once
#endif // _MSC_VER > 1000
// OwnDrawSlider.h : header file
//
/////////////////////////////////////////////////////////////////////////////
// COwnDrawSlider window
class COwnDrawSlider : public CSliderCtrl
{
// Construction
public:
COwnDrawSlider();
// Attributes
public:
CBrush black_brush;
// Operations
public:
// Overrides
// ClassWizard generated virtual function overrides
//{{AFX_VIRTUAL(COwnDrawSlider)
//}}AFX_VIRTUAL
// Implementation
public:
virtual ~COwnDrawSlider();
// Generated message map functions
protected:
//{{AFX_MSG(COwnDrawSlider)
afx_msg void OnPaint();
afx_msg HBRUSH OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor);
afx_msg BOOL OnEraseBkgnd(CDC* pDC);
afx_msg void OnLButtonDown(UINT nFlags, CPoint point);
//}}AFX_MSG
DECLARE_MESSAGE_MAP()
};
/////////////////////////////////////////////////////////////////////////////
//{{AFX_INSERT_LOCATION}}
// Microsoft Visual C++ will insert additional declarations immediately before the previous line.
#endif // !defined(AFX_OWNDRAWSLIDER_H__82981708_4CBC_4D1E_8982_504E99BE489D__INCLUDED_)
Here is the OwnDrawSlider class cpp:
// OwnDrawSlider.cpp : implementation file
//
#include "stdafx.h"
#include "OwnDrawSlider.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
/////////////////////////////////////////////////////////////////////////////
// COwnDrawSlider
COwnDrawSlider::COwnDrawSlider()
{
black_brush.CreateSolidBrush(RGB(0,0,0));
}
COwnDrawSlider::~COwnDrawSlider()
{
}
BEGIN_MESSAGE_MAP(COwnDrawSlider, CSliderCtrl)
//{{AFX_MSG_MAP(COwnDrawSlider)
ON_WM_PAINT()
ON_WM_CTLCOLOR()
ON_WM_ERASEBKGND()
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
/////////////////////////////////////////////////////////////////////////////
// COwnDrawSlider message handlers
void COwnDrawSlider::OnPaint()
{
CPaintDC dc(this); // device context for painting
// TODO: Add your message handler code here
CRect channelRect,thumbRect;
GetChannelRect(&channelRect);
GetThumbRect(&thumbRect);
GetClientRect(&channelRect);
thumbRect=channelRect;
channelRect.top+=5;
channelRect.bottom-=5;
int mid=(channelRect.top+channelRect.bottom)/2;
int w=channelRect.right-channelRect.left;
int pos=GetPos();
int minVal,maxVal;
GetRange(minVal,maxVal);
thumbRect.left=(pos-minVal)*w/(maxVal-minVal);
thumbRect.right=thumbRect.left+(channelRect.right-channelRect.left)*0.025f;
thumbRect.top+=12;
thumbRect.bottom-=12;
//draw the channel
//CBrush whiteBrush(RGB(255,255,255));
//dc.SelectObject(whiteBrush);
//dc.FrameRect(&channelRect,&whiteBrush);
//draw the thumb
CBrush yellowBrush(RGB(255,255,0));
dc.SelectObject(yellowBrush);
dc.Ellipse(&thumbRect);
CFont myFont;
myFont.CreatePointFont(96,"Tahoma");
dc.SetBkColor(RGB(0,0,0));
dc.SetTextColor(RGB(255,255,255));
dc.SelectObject(&myFont);
char msg[128];
sprintf(msg,"%d",pos);
CRect msgRect=thumbRect;
msgRect.top=thumbRect.bottom;
msgRect.bottom=msgRect.top+16;
int mid0=(thumbRect.left+thumbRect.right)/2;
msgRect.left=mid0-6*strlen(msg);
msgRect.right=mid0+6*strlen(msg);
dc.DrawText(msg,strlen(msg),&msgRect,DT_CENTER);
sprintf(msg,"%d",minVal);
msgRect.left=channelRect.left;
msgRect.right=channelRect.left+12*strlen(msg);
dc.DrawText(msg,strlen(msg),&msgRect,DT_LEFT);
sprintf(msg,"%d",maxVal);
msgRect.left=channelRect.right-12*strlen(msg);
msgRect.right=channelRect.right;
dc.DrawText(msg,strlen(msg),&msgRect,DT_RIGHT);
//draw the axis
CPen yellowPen(PS_SOLID,3,RGB(255,255,255));
dc.SelectObject(yellowPen);
dc.MoveTo(channelRect.left,mid);
dc.LineTo(channelRect.right,mid);
dc.MoveTo(channelRect.left,channelRect.top+12);
dc.LineTo(channelRect.left,channelRect.bottom-12);
dc.MoveTo(channelRect.right,channelRect.top+12);
dc.LineTo(channelRect.right,channelRect.bottom-12);
CPen thinPen(PS_SOLID,1,RGB(255,255,0));
dc.SelectObject(thinPen);
int numTick=GetNumTics();
DWORD* ptick=GetTicArray();
float pixelPerTick=w*1.0/numTick;
for(int i=0;i<numTick;i++)
{
dc.MoveTo((ptick[i])*pixelPerTick+channelRect.left,mid-3);
dc.LineTo((ptick[i])*pixelPerTick+channelRect.left,mid+3);
}
// Do not call CSliderCtrl::OnPaint() for painting messages
}
HBRUSH COwnDrawSlider::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor)
{
HBRUSH hbr = CSliderCtrl::OnCtlColor(pDC, pWnd, nCtlColor);
// TODO: Change any attributes of the DC here
// TODO: Return a different brush if the default is not desired
return black_brush;
}
BOOL COwnDrawSlider::OnEraseBkgnd(CDC* pDC)
{
// TODO: Add your message handler code here and/or call default
RECT client;
GetClientRect(&client);
//COLORREF color=pDC->GetBkColor();
COLORREF color=::GetSysColor(COLOR_WINDOW);
//FillRect(pDC->m_hDC,&client,::GetStockObject(GRAY_BRUSH));
//pDC->FillSolidRect(&client,color);
pDC->FillSolidRect(&client,RGB(0,0,0));
return true;//do not call OnEraseBkgrnd, it will fill it white
//return CSliderCtrl::OnEraseBkgnd(pDC);
}
Create the slider using program (no resource associated with it):
pFrameSlider=new COwnDrawSlider();
pFrameSlider->Create(WS_CHILD|WS_VISIBLE|BS_OWNERDRAW|TBS_HORZ|TBS_AUTOTICKS,rButton,this,ID_FRAME_SLIDER);
//pFrameSlider->SizeToContent();
pFrameSlider->SetTicFreq(50);
pFrameSlider->ShowWindow(SW_SHOW);
In the parent window, the NM_RELEASECAPTURE is trapped. When created, the rect=[9,57,384,822]. The rect obtained by the GetChannelRect is [19,23, 8, 40]. and the mouse operation is limited in this rect.
My question is: why the two rect are so different? How can I change the channel rect by program (without any resource associated with it)?
How would you go about painting something onto a window in regular intervals.
I have come up with the this (stripped quite a bit for clarity)
#include <windows.h>
void DrawOntoDC (HDC dc) {
pen = CreatePen(...)
penOld = SelectObject(dc, pen)
..... Here is the actual drawing, that
..... should be regurarly called, since
..... the drawn picture changes as time
..... progresses
SelectObject(dc, pen_old);
DeleteObject(pen);
}
LRESULT CALLBACK WindowProc(....) {
switch(Msg) {
case WM_PAINT: {
PAINTSTRUCT ps;
dc = BeginPaint(hWnd, &ps);
..... A Memory DC is created
..... In order to prevent flickering.
HBITMAP PersistenceBitmap;
PersistenceBitmap = CreateCompatibleBitmap(dc, windowHeight, windowHeight);
HDC dcMemory = CreateCompatibleDC(dc);
HBITMAP oldBmp = (HBITMAP) SelectObject(dcMemory, PersistenceBitmap);
DrawOntoDC(dcMemory);
..... "copying" the memory dc in one go unto dhe window dc:
BitBlt ( dc,
0, 0, windowWidth, windowHeight,
dcMemory,
0, 0,
SRCCOPY
);
..... destroy the allocated bitmap and memory DC
..... I have the feeling that this could be implemented
..... better, i.e. without allocating and destroying the memroy dc
..... and bitmap with each WM_PAINT.
SelectObject(dcMemory, oldBmp);
DeleteDC(dcMemory);
DeleteObject(PersistenceBitmap);
EndPaint (hWnd, &ps);
return 0;
}
default:
return DefWindowProc(hWnd, Msg, wParam, lParam);
}
}
DWORD WINAPI Timer(LPVOID p) {
..... The 'thread' that makes sure that the window
..... is regularly painted.
HWND hWnd = (HWND) *((HWND*) p);
while (1) {
Sleep(1000/framesPerSecond);
InvalidateRect(hWnd, 0, TRUE);
}
}
int APIENTRY WinMain(...) {
WNDCLASSEX windowClass;
windowClass.lpfnWndProc = WindowProc;
windowClass.lpszClassName = className;
....
RegisterClassEx(&windowClass);
HWND hwnd = CreateWindowEx(
....
className,
....);
ShowWindow(hwnd, SW_SHOW);
UpdateWindow(hwnd);
DWORD threadId;
HANDLE hTimer = CreateThread(
0, 0,
Timer,
(LPVOID) &hwnd,
0, &threadId );
while( GetMessage(&Msg, NULL, 0, 0) ) {
....
}
return Msg.wParam;
}
I guess there's a lot that could be improved and I'd appreciate any pointer to things I have overlooked.
Doing this kind of thing with a worker thread is not optimal.
Given that the optimal code path for painting is always via a WM_PAINT that leaves two ways to do this:
Simply create a timer on the GUI thread, post WM_TIMER messages to a timerproc, or the window directly, and invoke the OnTick() part of your engine. IF any sprites move, they invalidate their area using InvalidateRect() and windows follows up by automatically posting a WM_PAINT. This has the advantage of having a very low CPU usage if the game is relatively idle.
Most games want stricter timing that can be achieved using a low priority WM_TIMER based timer. In that case, you implement a game loop something like this:
Message Loop:
while(stillRunning)
{
DWORD ret = MsgWaitForMultipleObjects(0,NULL,FALSE,frameIntervalMs,QS_ALLEVENTS);
if(ret == WAIT_OBJECT_0){
while(PeekMessage(&msg,0,0,0,PM_REMOVE)){
TranslateMessage(&msg);
DispatchMessage(&msg);
}
if(TickGame()) // if TickGame indicates that enough time passed for stuff to change
RedrawWindow(hwndGame,...); // Dispatch a WM_PAINT immediately.
}
The danger with this kind of message loop is, if the ... application goes into any kind of modal state :- the user starts to drag the window / a modal dialog box pops up, then messages are being pumped by the modal loop, so the animation stops. As a result you need to have a fallback timer if you need to mix a high performance message loop with modal operations.
WRT your WM_PAINT implementation - its usually better to (re)create your backbuffer in response to WM_SIZE messages. That way its always the right size, and you don't incurr the rather large cost of recreating a large memory buffer many times per second.
Borrowing bits and pieces from various places, I came up with the following approach for my similar problem. The following is a test application to test the concept.
In this test application I am using an MFC static window which I am updating with a text string by periodically calling the ::SetWindowText() function with the MFC static window's handle. This works fine to display a marching right angle bracket to demonstrate that the animation works.
In the future I intend on using a memory resident bitmap image which is modified in the animation loop which is then posted into a bitmap attached to the static text window. This technique allows for an animated bitmap to be presented with a more elegant indication of something in progress.
Testing this concept was done with an MFC dialog application that contains two static windows for the progress indicators and two additional buttons, Start and Stop, to start and stop the progress indicators. The goal is that when the Start button is pressed a series of greater than signs are written across the static window and then cleared and then once more started. So the animation looks like an LED sign that has arrows marching across from left to right in a horizontal marque.
These two buttons do nothing more than set an indicator in the animation object to be either one (on) or zero (off). The animation object thread which does the actual animation just reads from the m_state variable and does not modify it.
The timer delay quantity is hardcoded for the purposes of this test. It could easily be a parameter.
The dialog is still responsive and even as it is updating I can display the default About Box for the dialog application and move the About Box around. I can also drag the dialog application itself around screen (without the About Box displayed as that is a modal dialog) with the static window still updating.
The Animation class source
The source code for the animation logic is a simple class that starts a thread which then updates the specified dialog control. While the animation loop is a static method of the class, the data used by the loop is specified in the object itself so multiple animations can be done with different objects using the same static loop.
This approach is fairly straightforward and simple. Rather than using a more sophisticated approach with a thread pool and a timer pool, it dedicates a thread and a timer object to a single animation. It appears obvious that this approach will not scale well however for an application with a couple of animations, it works well enough.
class AnimatedImage
{
UINT m_state; // current on/off state of the animation. if off (0) then the window is not updated
UINT m_itemId; // control identifier of the window that we are going to be updating.
HWND m_targetHwnd; // window handle of the parent dialog of the window we are going to be updating
UINT m_i; // position for the next right angle bracket
wchar_t m_buffer[32]; // text buffer containing zero or more angle brackets which we use to update the window
DWORD m_lastError; // result of GetLastError() in case of an error.
HANDLE m_hTimer; // handle for the timer
HANDLE m_hThread; // handle for the thread created.
LARGE_INTEGER m_liDueTime; // time delay between updates
public:
AnimatedImage(UINT itemId = 0, HWND hWnd = NULL) : m_state(0), m_itemId(itemId), m_targetHwnd(hWnd), m_i(0), m_lastError(0), m_hTimer(NULL), m_hThread(NULL) { memset(m_buffer, 0, sizeof(m_buffer)) ; }
~AnimatedImage() { Kill(); CloseHandle(m_hTimer); CloseHandle(m_hThread); } // clean up the timer and thread handle.
static unsigned __stdcall loop(AnimatedImage *p); // animation processing loop
void Run(); // starts the animation thread
void Start(); // starts the animation
void Stop(); // stops the animation
void Kill(); // indicates the thread is to exit.
// Functions used to get the target animation window handle
// and to set the parent window handle and the dialog control identifier.
// This could be simpler by just requiring the target animation window handle
// and letting the user do the GetDlgItem() function themselves.
// That approach would make this class more flexible.
HWND GetImageWindow() { return ::GetDlgItem(m_targetHwnd, m_itemId); }
void SetImageWindow(UINT itemId, HWND hWnd) { m_itemId = itemId; m_targetHwnd = hWnd; }
};
unsigned __stdcall AnimatedImage::loop(AnimatedImage *p)
{
p->m_liDueTime.QuadPart = -10000000LL;
// Create an unnamed waitable timer. We use this approach because
// it makes for a more dependable timing source than if we used WM_TIMER
// or other messages. The timer resolution is much better where as with
// WM_TIMER is may be no better than 50 milliseconds and the reliability
// of getting the messages regularly can vary since WM_TIMER are lower
// in priority than other messages such as mouse messages.
p->m_hTimer = CreateWaitableTimer(NULL, TRUE, NULL);
if (NULL == p->m_hTimer)
{
return 1;
}
for (; ; )
{
// Set a timer to wait for the specified time period.
if (!SetWaitableTimer(p->m_hTimer, &p->m_liDueTime, 0, NULL, NULL, 0))
{
p->m_lastError = GetLastError();
return 2;
}
// Wait for the timer.
if (WaitForSingleObject(p->m_hTimer, INFINITE) != WAIT_OBJECT_0) {
p->m_lastError = GetLastError();
return 3;
}
else {
if (p->m_state < 1) {
p->m_i = 0;
memset(p->m_buffer, 0, sizeof(m_buffer));
::SetWindowText(p->GetImageWindow(), p->m_buffer);
}
else if (p->m_state < 2) {
// if we are updating the window then lets add another angle bracket
// to our text buffer and use SetWindowText() to put it into the
// window area.
p->m_buffer[p->m_i++] = L'>';
::SetWindowText(p->GetImageWindow(), p->m_buffer);
p->m_i %= 6; // for this demo just do a max of 6 brackets before we reset.
if (p->m_i == 0) {
// lets reset our buffer so that the next time through we will start
// over in position zero (first position) with our angle bracket.
memset(p->m_buffer, 0, sizeof(m_buffer));
}
}
else {
// we need to exit our thread so break from the loop and return.
break;
}
}
}
return 0;
}
void AnimatedImage::Run()
{
m_hThread = (HANDLE)_beginthreadex(NULL, 0, (_beginthreadex_proc_type)&AnimatedImage::loop, this, 0, NULL);
}
void AnimatedImage::Start()
{
m_state = 1;
}
void AnimatedImage::Stop()
{
m_state = 0;
}
void AnimatedImage::Kill()
{
m_state = 3;
}
How the class is used
For this simple test dialog application, we just create a couple of global objects for our two animations.
AnimatedImage xxx;
AnimatedImage xx2;
In the OnInitDialog() method of the dialog application the animations are initialized before returning.
// TODO: Add extra initialization here
xxx.SetImageWindow(IDC_IMAGE1, this->m_hWnd);
xxx.Run();
xx2.SetImageWindow(IDC_IMAGE2, this->m_hWnd);
xx2.Run();
return TRUE; // return TRUE unless you set the focus to a control
There are two button click handlers which handle a click on either the Start button or the Stop button.
void CMFCApplication2Dlg::OnBnClickedButton1()
{
// TODO: Add your control notification handler code here
xxx.Start();
xx2.Start();
}
void CMFCApplication2Dlg::OnBnClickedButton2()
{
// TODO: Add your control notification handler code here
xxx.Stop();
xx2.Stop();
}
The dialog application main dialog resource is defined as follows in the resource file.
IDD_MFCAPPLICATION2_DIALOG DIALOGEX 0, 0, 320, 200
STYLE DS_SETFONT | DS_FIXEDSYS | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME
EXSTYLE WS_EX_APPWINDOW
FONT 8, "MS Shell Dlg", 0, 0, 0x1
BEGIN
DEFPUSHBUTTON "OK",IDOK,209,179,50,14
PUSHBUTTON "Cancel",IDCANCEL,263,179,50,14
CTEXT "TODO: Place dialog controls here.",IDC_STATIC,10,96,300,8
LTEXT "Static",IDC_IMAGE1,7,7,110,21
LTEXT "Static",IDC_IMAGE2,64,43,112,27
PUSHBUTTON "Start",IDC_BUTTON1,252,16,50,19
PUSHBUTTON "Stop",IDC_BUTTON2,248,50,57,21
END