Skip to main content

C++ Barcode Reader Tutorial

Introduction

In this tutorial, we will be creating a simple console application in C++ that opens, claims and enables a scanner device and provides a mechanism to see what labels were scanned with that device. This tutorial only covers a very basic aspect of development using OPOS with C++, but should suffice as a starting point for any application that looks to read labels from a Datalogic scanning device.

Prerequisites

Developing an application for a Datalogic scanning device requires you to first have Datalogic's OPOS installed. The OPOS installer ensures the required ActiveX Controls are registered and any supporting files are present.

Creating a project

  1. Using Visual Studio 2019, navigate to File > New > Project ...

  2. In the Create a new project dialog, set the filter to C++, select Console App and select Next

  3. In the Configure your new project dialog, declare your Project name, Location and Solution name as desired and then select Create.

Exposing OPOS control objects

The very first step is to reference the scanner ActiveX Control by its ProgID (Programmatic Identifier) using the #import directive.

#import "progid:OPOS.Scanner"

int main()
{
return 0;
}

Compiling the project at this point, the #import directive generates two files in your $(Configuration) folder (i.e. Debug/Release). OPOSScanner.tlh is the Type Library Header and OPOSScanner.tli is the Type Library Interface. These files contain generated type information that bridges the COM object's interface with C++ language constructs.

While this makes the compiler aware of the COM object you're accessing, you'll likely find it helpful to make the Visual Studio IDE aware of of it as well; to do this you'll need to import the type library. Inside OPOSScanner.tlh are a series of forward declared structs. The first struct reads:

struct __declspec(uuid("<some-unique-identifier>"))
/* LIBID */ __OposScanner_CCO;

Adding #import "libid:<some-unique-identifier>" to your project will inform the IDE of the COM interface and everything in the namespace OposScanner_CCO, but is not required for compilation.

Now your application should look similar to:

// This import statement causes generation of OPOSScanner.tlh and OPOSScanner.tli
#import "progid:OPOS.Scanner"

// This import statement informs the IDE of the COM interface and everything in the
// namespace OposScanner_CCO, but is not required for compilation.
#import "libid:ccb90180-b81e-11d2-ab74-0040054c3719"

int main()
{
return 0;
}

Creating the OPOS scanner interface

CoInitializeEx() must be called near the start of program to load and initialize the COM library and CoUninitialize() must be called near the end of the program to unload the COM library.

The scanner is implemented using the smart pointer IOPOSScannerPtr defined in OPOSScanner.tlh as _COM_SMARTPTR_TYPEDEF(IOPOSScanner, __uuidof(IOPOSScanner)), based on the _com_ptr_t class.

An instance of the scanner is created by calling _com_ptr_t::CreateInstance(const char *progid) and is release by calling _com_ptr_t::Release() when no longer needed. Failure to release the object can cause problems with an actual POS system.

// This import statement causes generation of OPOSScanner.tlh and OPOSScanner.tli
#import "progid:OPOS.Scanner"

// This import statement informs the IDE of the COM interface and everything in the
// namespace OposScanner_CCO, but is not required for compilation.
#import "libid:ccb90180-b81e-11d2-ab74-0040054c3719"

int main()
{
// Load and initialize the COM library.
CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);

// Create a COM object and point to it.
OposScanner_CCO::IOPOSScannerPtr scanner;
scanner.CreateInstance("OPOS.Scanner");

//
// more to come ....
//

// Release the COM object
scanner.Release();

// Unload the COM library.
CoUninitialize();
return 0;
}

Accessing the OPOS Scanner interface

At this point, you are ready to use the scanner object.

The OPOS standard defines a general sequence to access a device:

  • open: open a context to the device
  • claim: claim control over the device
  • enable: enable the device's operation
  • disable: disable the device's operation
  • release: release control over the device
  • close: close the context to the device

Opening a scanner context requires you to reference a device profile (a device profile simply being a name representing a set of parameters relevant to the device). When OPOS is installed, scanner profiles are seen as the subkey names installed under the UPOS-specified registry key HKEY_LOCAL_MACHINE\Software\Wow6432Node\OLEforRetail\ServiceOPOS\SCANNER. It is your choice whether you want to programmatically obtain the profile names from the registry or to simply hard-code them in your application.

If the scanner context is successfully opened, you can then claim the device, gaining exclusive access to it. And if the device is successfully claimed, you can then enable it to perform subsequent operations.

#include <string>

// This import statement causes generation of OPOSScanner.tlh and OPOSScanner.tli
#import "progid:OPOS.Scanner"

// This import statement informs the IDE of the COM interface and everything in the
// namespace OposScanner_CCO, but is not required for compilation.
#import "libid:ccb90180-b81e-11d2-ab74-0040054c3719"

int main()
{
// Load and initialize the COM library.
CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);

// Create a COM object and point to it.
OposScanner_CCO::IOPOSScannerPtr scanner;
scanner.CreateInstance("OPOS.Scanner");

// Open a context with the scanner (e.g. "USBScanner").
std::string profileName("<your selected profile>");
scanner->Open(profileName.c_str());

// Claim control of the scanner using a 1000 millisecond timeout.
scanner->ClaimDevice(1000L);
if (scanner->Claimed)
{
// Enable the device, label decoding and transmission of event data.
scanner->DeviceEnabled = true;
scanner->DataEventEnabled = true;
scanner->DecodeData = true;

//
// more to come ....
//

// Disable, release and close the scanner.
scanner->DeviceEnabled = false;
scanner->ReleaseDevice();
scanner->Close();
}

// Release the COM object
scanner.Release();

// Unload the COM library.
CoUninitialize();
return 0;
}

Creating a sink for the scanner

To make the application useful, we must create a scanner sink (an object that receives input from the device).

  1. Using the Visual Studio 2019 Solution Explorer, right mouse click Header Files

  2. And when the dialog appears add a header file named ScannerSink.h

  3. Using the Visual Studio 2019 Solution Explorer, right mouse click Source Files

  4. And when the dialog appears add a source file named ScannerSink.cpp

The sink provides callback functions for use by the COM object. The callback functions provide implementations for the virtual methods of

and the dispatch methods of

  • _IOPOSScannerEvents (defined in OPOSScanner.tli)

You will also notice enum ScannerEvent whose values are used by the COM object to map events to the dispatch methods. These will be discussed later in Implementing IDispatch.

The scanner sink is derived from OposScanner_CCO::_IOPOSScannerEvents as follows

// ScannerSink.h

// This import statement causes generation of OPOSScanner.tlh and OPOSScanner.tli
#import "progid:OPOS.Scanner"

// This import statement informs the IDE of the COM interface and everything in the
// namespace OposScanner_CCO, but is not required for compilation.
#import "libid:ccb90180-b81e-11d2-ab74-0040054c3719"

class ScannerSink: public OposScanner_CCO::_IOPOSScannerEvents
{
public:

ScannerSink(OposScanner_CCO::IOPOSScanner &scannerObject)
: scanner(scannerObject)
, ref(0)
{};

// IUnknown methods
IFACEMETHODIMP QueryInterface(REFIID riid, void **ppv);
IFACEMETHODIMP_(ULONG) AddRef();
IFACEMETHODIMP_(ULONG) Release();

// IDispatch methods
IFACEMETHODIMP GetTypeInfoCount(UINT *pctinfo);
IFACEMETHODIMP GetTypeInfo(UINT itinfo,
LCID lcid, ITypeInfo **iti);
IFACEMETHODIMP GetIDsOfNames(REFIID riid, LPOLESTR *names,
UINT size, LCID lcid,DISPID *rgDispId);
IFACEMETHODIMP Invoke(DISPID dispid, REFIID riid, LCID lcid,
WORD flags,DISPPARAMS *dispparams, VARIANT *result,
EXCEPINFO *exceptioninfo, UINT *argerr);

// _IOPOSScannerEvents methods
HRESULT DataEvent(long Status);
HRESULT DirectIOEvent(
long EventNumber,
long *Data,
BSTR *String);
HRESULT ErrorEvent(
long ResultCode,
long ResultCodeExtended,
long ErrorLocus,
long *ErrorResponse);
HRESULT StatusUpdateEvent(
long Data);

enum ScannerEvent: DISPID
{
Unused = 0,
Data = 1,
DirectIO = 2,
Error = 3,
Reserved = 4,
StatusUpdate = 5,
Count = 6
};

private:

LONG ref;
OposScanner_CCO::IOPOSScanner &scanner;
};

Implementing IUnknown

IUnknown is inherited by every COM interface. IUnknown has three virtual methods:

  • AddRef()
  • QueryInterface()
  • Release()

AddRef() and Release() are used for reference counting and QueryInterface() retrieves the supported interfaces of an object. The implementation of IUnknown is often as straight-forward as seen in this tutorial, which is loosely based off of the MSDN article Implementing IUnknown in C++.

// ScannerSink.cpp
#include "ScannerSink.h"

IFACEMETHODIMP ScannerSink::QueryInterface(REFIID riid, void **ppv)
{
*ppv = nullptr;
IID id = __uuidof(OposScanner_CCO::_IOPOSScannerEvents);
HRESULT hr = E_NOINTERFACE;
if (riid == IID_IUnknown || riid == IID_IDispatch || riid == id) {
*ppv = static_cast<OposScanner_CCO::_IOPOSScannerEvents *>(this);
AddRef();
hr = S_OK;
}
return hr;
}

IFACEMETHODIMP_(ULONG) ScannerSink::AddRef()
{
return ++ref;
}

IFACEMETHODIMP_(ULONG) ScannerSink::Release()
{
if (--ref == 0)
delete this;
return ref;
}

Implementing IDispatch

IDispatch has four virtual methods:

  • Invoke()
  • GetIDsOfNames()
  • GetTypeInfo()
  • GetTypeInfoCount()

The COM object calls GetIDsOfNames() to obtain the dispatch id (DISPID) of a single class property or method. The dispatch id is then used by the COM object during subsequent calls to Invoke().

Invoke() allows the COM object to call the _IOPOSScannerEvents methods of the sink class. The DISPID parameter identifies the method to call and DISPPARAMS contains the values to be passed to the function.

GetTypeInfo() and GetTypeInfoCount() are used to retrieve type information, but provide no value in our case.

// ScannerSink.cpp
IFACEMETHODIMP ScannerSink::GetTypeInfoCount(UINT *pctinfo)
{
*pctinfo = 0;
return E_NOTIMPL;
}

IFACEMETHODIMP ScannerSink::GetTypeInfo(UINT itinfo, LCID lcid, ITypeInfo **iti)
{
*iti = nullptr;
return E_NOTIMPL;
}

IFACEMETHODIMP ScannerSink::GetIDsOfNames(REFIID riid, LPOLESTR *names,
UINT size, LCID lcid, DISPID *dispids)
{
if (wcscmp(names[0], L"StatusUpdateEvent") == 0)
dispids[0] = ScannerEvent::StatusUpdate;
else if (wcscmp(names[0], L"DirectIOEvent") == 0)
dispids[0] = ScannerEvent::DirectIO;
else if (wcscmp(names[0], L"ErrorEvent") == 0)
dispids[0] = ScannerEvent::Error;
else if (wcscmp(names[0], L"DataEvent") == 0)
dispids[0] = ScannerEvent::Data;
else
dispids[0] = -1;

return ((dispids[0] == -1) ? E_NOTIMPL : S_OK);
}

IFACEMETHODIMP ScannerSink::Invoke(DISPID dispid, REFIID riid, LCID lcid,
WORD flags, DISPPARAMS *dispparams, VARIANT *result,
EXCEPINFO *exceptioninfo, UINT *argerr)
{
if (ScannerEvent::Data == dispid)
return DataEvent(dispparams->rgvarg[0].lVal);
else
return S_OK;
}

Implementing event functions

With IDispatch implemented the scanner sink will invoke its supported events. Per the UPOS specification, when the sink invokes a data event, subsequent data events are automatically suspended because OPOSScanner.DataEventEnabled is set to false, resulting in queueing of subsequent data events. DataEventEnabled must be reset to true to continue receiving more data events.

For the purpose of this tutorial, the scanned label data will simply be printed to the console.

// ScannerSink.cpp
HRESULT ScannerSink::DataEvent(long Status)
{
std::cout << "Data: " << scanner.ScanDataLabel << std::endl;
scanner.DataEventEnabled = true;
return S_OK;
}

Connecting the scanner and the sink

Now we must inform the scanner of the sink. To do this query an IConnectionPointContainer interface by calling QueryInterface() from the scanner. On the connection point container, get the interface of the _IOPOSScannerEvents connection point by calling FindConnectionPoint(). Now a connection can be established between the scanner and the sink by calling Advise() on the connection point.

The following code can be seen in context in the segment Tying it all together.

IConnectionPointContainer *cpc;
scanner->QueryInterface(IID_IConnectionPointContainer, (void **) &cpc);

IConnectionPoint *cp;
cpc->FindConnectionPoint(__uuidof(OposScanner_CCO::_IOPOSScannerEvents), &cp);
cpc->Release();

ScannerSink *sink = new ScannerSink(*scanner);
DWORD cookie;
cp->Advise(sink, &cookie);

Tying it all together

As any experienced developer knows, a huge part of writing code is defensive programming, guarding against potential errors. So be aware, for the sake of brevity, this tutorial has avoided addressing the myriad issues you may encounter.

That said, a more complete example can be found in the Datalogic OPOS Examples.

Refactoring a little, we can move shared #include and #import statements to a common location (ScannerSink.h in this tutorial).

// ScannerSink.h
#include <windows.h>
#include <string>
#include <iostream>

// This import statement causes generation of OPOSScanner.tlh and OPOSScanner.tli
#import "progid:OPOS.Scanner"

// This import statement informs the IDE of the COM interface and everything in the
// namespace OposScanner_CCO, but is not required for compilation.
#import "libid:ccb90180-b81e-11d2-ab74-0040054c3719"

Doing that simplifies the main file a little. Finally, we can add the code connecting the scanner to the sink.

#include "ScannerSink.h"
#include <processthreadsapi.h>

static DWORD threadID;
static BOOL handler(DWORD event);

int main()
{
// Setup the console program to exit gracefully.
threadID = GetCurrentThreadId();
SetConsoleCtrlHandler((PHANDLER_ROUTINE)(handler), TRUE);

// Load and initialize the COM library.
CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);

// Create a COM object and point to it.
OposScanner_CCO::IOPOSScannerPtr scanner;
scanner.CreateInstance("OPOS.Scanner");

// Open a context with the scanner (e.g. "USBScanner").
std::string profileName("<your selected profile>");
scanner->Open(profileName.c_str());

// Claim control of the scanner using a 1000 millisecond timeout.
scanner->ClaimDevice(1000L);
if (scanner->Claimed)
{
// The scanner has been opened and claimed.

// Enable the device, label decoding and transmission of event data.
scanner->DeviceEnabled = true;
scanner->DataEventEnabled = true;
scanner->DecodeData = true;

// Determine whether scanner is connectable
IConnectionPointContainer* cpc;
bool isConnectable = (scanner->QueryInterface(IID_IConnectionPointContainer, (void**)&cpc) == S_OK);

if (isConnectable)
{
// Determine whether _IOPOSScannerEvents connection point is supported.
IConnectionPoint* cp;
bool haveConnectionPoint = (cpc->FindConnectionPoint(__uuidof(OposScanner_CCO::_IOPOSScannerEvents), &cp) == S_OK);
cpc->Release();

if (haveConnectionPoint)
{
ScannerSink* sink = new ScannerSink(*scanner);

// Connect cp with sink (subscribe to the sink).
// cookie is a token representing the connection,
// used later when deleting the connection.
DWORD cookie;
cp->Advise(sink, &cookie);

std::cout << "Press \'Ctrl + C\' to quit." << std::endl;

// The scanner message loop. Events will be handled by the methods of the sink.
static MSG msg = { 0 };
while (GetMessage(&msg, 0, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}

// Delete the connection (unsubscribe from the sink).
cp->Unadvise(cookie);
cp->Release();
}
}

// Disable, release and close the scanner.
scanner->DeviceEnabled = false;
scanner->ReleaseDevice();
scanner->Close();
}

// Release the COM object
scanner.Release();

// Unload libraries on this thread.
CoUninitialize();
return 0;
}

BOOL handler(DWORD event)
{
PostThreadMessage(threadID, WM_QUIT, 0, 0);
return TRUE;
}