Rendering a Region Dialog From a BMP/JPEG

Environment: VC6/VC7, MS Platform Core SDK, 24/32 bit True Color Windows Desktop Environment. (Test on Win2K/XP Passed, No Guarantee on Win95/98/ME)

Key Technology Used: Bitmap/JPEG Image Processing, Advanced GUI, Shell Programming (System Tray), Waitable Timer, Multithread, Win2k User Context Programmming (Lock & Shut Down System)

Applicable Article Category in CodeGuru: GDI, Dialog, System, Miscellaneous, Sample

Summary and GUI

It seems there are quite a few Region-Based Dialog samples here already; unfortunately, none of them went one step further to permit the user to render a region from both image resource and images files such as bitmap and JPEG. So, this time I would present a much more flexible and user-friendly Region Dialog that support such things. With it, you can drag and drop to apply new images; press Ctrl+Z to go back through all the images you have used, select the background color on different files, and even zoom the images to any ratio you like. Besides, I added some accessory functionalities such as "Lock Machine," "Shut Down Machine After n Minutes," "Start Screen Saver," and so on. Well, have a look at the GUI, please:

Figure 1—GUI overview of Customizable Region Dialog

The user is free to choose the background color to render region from a bitmap or jpeg file. Say, by using a Fuchsia color, we apply this color to the middle image, and get the right one.

Figure 2—System Tray Context Menu of Customizable Region Dialog

In its context menu from the system tray, the user can, just as shown in the above figure, lock the machine, start a screen saver, and shut down the machine in a different time and a different way. Besides, you can set the balloon message and pop up period so that it can remind you of something, like this one:

Figure 3—Balloon Message Box from System Tray (text from Red Alert ©Westwood Studio)

For enjoyment, I have added more than a dozen of character images with the demo. When you want to switch images, just activate the window by a mouse click, press Ctrl+Z to use the previous image, or you can drag an image file and drop it to the dialog to apply it immediately. If you want to make your own image, you have to do it pixel by pixel, replacing the background with the background color; please use a bitmap as much as possible because a JPEG image is not so "sharp." Following are some characters used in the demo:



Click here for a larger image.

Figure 4—Various Images Bounded With Customizable Region Dialog Demo

Architecture

Basically, it is a dialog with a system tray icon; it reads image files from disk, makes the region rendered, and adjusts the show of the dialog according to the zoom ration and transparency ratio. Under the hood, a thread is using a waitable timer to check whether the user wants to shut down the machine at some time.

Implementation Description

Reading BMP/JPEG Image Disk Files and Rendering Its Region

The only way to do so is to scan every pixel inside the image and compare its color with the background color (or filter color, if you like this name). If they're the same, do not add the point to the region; otherwise, add it to the region. But, region operation is so time consuming, and adding one point each time is terrible when scanning a big image file. So, Mr. David Gallardo Llopis's article Technique to Create Dialogs from Images does a region operation only once if we could find a consecutive serial of points to form an rectangle of background color pixels. Unfortunately, his program only deals with images from bitmaps. So, I upgraded his routine to cope with both JPEG and Bitmap files: (Note: ONLY 24, 32 bit true color bitmap image is supported!!). To JPEG files, I use IOlePicture to read it from disk first, then use memory DC to transfer it to a bitmap (zoom it if applicable):

//Note: You must use true color DIB Bitmap -- 24 or 32 bit
//I just have no time to play with color table
HRGN DIBRegion(LPBITMAPINFOHEADER lpBmpInfoHead, LPVOID lpImage,
               COLORREF cTransparentColor,BOOL bIsTransparent)
{
    ASSERT(lpBmpInfoHead->biBitCount == 32 ||
           lpBmpInfoHead->biBitCount == 24);
    BYTE c_red = GetRValue(cTransparentColor);
    BYTE c_green = GetGValue(cTransparentColor);
    BYTE c_blue = GetBValue(cTransparentColor);

   // We create an empty region
    HRGN hRegion=NULL;
    //Here, please check Mr. David Gallardo Llopis's article's
    //sample code
    #define NUMRECT 100
    DWORD maxRect = NUMRECT;

   // We create the memory data
    HANDLE hData=GlobalAlloc(GMEM_MOVEABLE,sizeof(RGNDATAHEADER)+
                            (sizeof(RECT)*maxRect));
    RGNDATA *pData=(RGNDATA*) GlobalLock(hData);
    pData->rdh.dwSize=sizeof(RGNDATAHEADER);
    pData->rdh.iType=RDH_RECTANGLES;
    pData->rdh.nCount=pData->rdh.nRgnSize=0;
    SetRect(&pData->rdh.rcBound,MAXLONG,MAXLONG,0,0);
    //Handling Bitmap --- oop, it is my code now
    DWORD dwBytePerPixel = lpBmpInfoHead->biBitCount/8;
    // 3 or 4
    DWORD dwBytePerLine = (lpBmpInfoHead->biWidth *
                           dwBytePerPixel);
    while(dwBytePerLine%4)
      dwBytePerLine++;
    DWORD dwSize = dwBytePerLine * lpBmpInfoHead->biHeight;

    BYTE *Pixeles=(BYTE*)lpImage;
    Pixeles += dwBytePerLine * (lpBmpInfoHead->biHeight - 1);
    // Main loop
    for(int Row=0;Row<lpBmpInfoHead->biHeight;Row++)
    {
      // Horizontal loop
      for(int Column=0;Column<lpBmpInfoHead->biWidth;Column++)
      {
        // We optimized searching for adjacent transparent pixels!
        int Xo=Column;
        LPBYTE lpByte = (LPBYTE)Pixeles;
        lpByte += Column*dwBytePerPixel;
        //Note Little Endian in Intel
        while(Column<lpBmpInfoHead->biWidth)
        {
          BOOL bInRange=FALSE;
          if(dwBytePerPixel == 4)    //32-bit bitmap
            {
              if(c_red == lpByte[2] && c_green ==
                          lpByte[1] && c_blue == lpByte[0])
                bInRange=TRUE;
            }
            else if(dwBytePerPixel == 3)    //24-bit bitmap
            {
              if(c_red == lpByte[2] && c_green ==
                          lpByte[1] && c_blue == lpByte[0])
                bInRange=TRUE;
            }
            if((bIsTransparent)  && (bInRange)) break;
            if((!bIsTransparent) && (!bInRange)) break;

            lpByte += dwBytePerPixel;
            Column++;
         }    // end while (Column < bm.bmWidth)

        if(Column>Xo)
        {
          if (pData->rdh.nCount>=maxRect)
          {
            GlobalUnlock(hData);
            maxRect+=NUMRECT;
          hData=GlobalReAlloc(hData,sizeof(RGNDATAHEADER)+
                             (sizeof(RECT)*maxRect),
                              GMEM_MOVEABLE);
            pData=(RGNDATA *)GlobalLock(hData);
          }

          RECT *pRect=(RECT*) &pData->Buffer;
          SetRect(&pRect[pData->rdh.nCount],Xo,Row,Column,Row+1);

          if(Xo<pData->rdh.rcBound.left)
          pData->rdh.rcBound.left=Xo;
          if(Row<pData->rdh.rcBound.top)
          pData->rdh.rcBound.top=Row;
          if(Column>pData->rdh.rcBound.right)
          pData->rdh.rcBound.right=Column;
          if(Row+1>pData->rdh.rcBound.bottom)
          pData->rdh.rcBound.bottom=Row+1;
          pData->rdh.nCount++;
          if(pData->rdh.nCount==2000)
          {
            HRGN hNewRegion=ExtCreateRegion(NULL,sizeof
                            (RGNDATAHEADER) + (sizeof(RECT) *
                            maxRect),pData);
            if (hNewRegion) {
              if (hRegion) {
                CombineRgn(hRegion,hRegion,hNewRegion,RGN_OR);
                DeleteObject(hNewRegion);
              } else
                hRegion=hNewRegion;
          }
          pData->rdh.nCount=0;
          SetRect(&pData->rdh.rcBound,MAXLONG,MAXLONG,0,0);
          }
        }    // if (Column > Xo)
      }      // for (int Column ...)
      Pixeles -= dwBytePerLine;
    }        // for (int Row...)

    HRGN hNewRegion=ExtCreateRegion(NULL,sizeof(RGNDATAHEADER)+
                    (sizeof(RECT)*maxRect),pData);
    if(hNewRegion)
    {
      // If the main region does already exists,
      // we add the new one
      if(hRegion)
      {
        CombineRgn(hRegion,hRegion,hNewRegion,RGN_OR);
        DeleteObject(hNewRegion);
      }
      else
        // if not, we consider the new one to be the main region
        // at first!
        hRegion=hNewRegion;
      }

      // We free the allocated memory and the rest of used
      // resources
      GlobalFree(hData);
      return hRegion;
}

Before calling this function, I have to read from JPEG/BMP file as following (to save space, checking code omitted)

HBITMAP hBmpOriginal = NULL;
if(strFilename.IsEmpty())    //strFilename is the image disk
                             //file name, use resource
{
  hBmpOriginal = (HBITMAP)::LoadImage(::AfxGetInstanceHandle(),
                            MAKEINTRESOURCE(IDB_SS),
    IMAGE_BITMAP, 0, 0, LR_CREATEDIBSECTION);
   m_imageType = res;
}
else if(strFilename.Right(3).CompareNoCase(_T("bmp"))
     == 0)    //bitmap file
{
  hBmpOriginal = (HBITMAP)::LoadImage(::AfxGetInstanceHandle(),
                  strFilename,
    IMAGE_BITMAP, 0, 0, LR_LOADFROMFILE);
  m_imageType = bmp;
}
else if(strFilename.Right(3).CompareNoCase(_T("jpg")) == 0 ||
  strFilename.Right(3).CompareNoCase(_T("jpeg")) == 0)
{
  HANDLE hFile = CreateFile((LPCTSTR)strFilename, GENERIC_READ, 0,
                 NULL, OPEN_EXISTING, 0, NULL);
  DWORD dwFileSize = GetFileSize(hFile, NULL);
  LPVOID pvData = NULL;
  HGLOBAL hGlobal = GlobalAlloc(GMEM_MOVEABLE, dwFileSize);
  pvData = GlobalLock(hGlobal);
  DWORD dwBytesRead = 0;
  BOOL bRead = ReadFile(hFile, pvData, dwFileSize, 
                        &dwBytesRead, NULL);
  GlobalUnlock(hGlobal);
  CloseHandle(hFile);

  LPSTREAM pstm = NULL;
  HRESULT hr = CreateStreamOnHGlobal(hGlobal, TRUE, &pstm);

  if(m_pPicture)
                    //LPPICTURE m_pPicture; set it to NULL
                    //when dialog created
  {
    m_pPicture->Release();
    m_pPicture = NULL;
  }
  hr = ::OleLoadPicture(pstm, dwFileSize, FALSE, IID_IPicture,
                       (LPVOID *)&m_pPicture);
  pstm->Release();
  GlobalFree(hGlobal);

  long hmWidth = 0L; long hmHeight = 0L;
  m_pPicture->get_Width(&hmWidth);
  m_pPicture->get_Height(&hmHeight);
  #define HIMETRIC_INCH 2540
  hMemDC = CreateCompatibleDC(NULL);
  int nWidth = MulDiv(hmWidth, GetDeviceCaps(hMemDC, LOGPIXELSX),
               HIMETRIC_INCH);
  int nHeight = MulDiv(hmHeight, GetDeviceCaps(hMemDC,
                       LOGPIXELSY), HIMETRIC_INCH);

  HDC hSelfDC = ::GetDC(this->GetSafeHwnd());
  //m:n is Zoom ratio
  hBmp = ::CreateCompatibleBitmap(hSelfDC, (int)(1.0*m*nWidth/n),
           (int)(1.0*m*nHeight/n));
  hPrevBmp = (HBITMAP)::SelectObject(hMemDC, hBmp);
  CRect rect;
  rect.SetRect(0,0,nWidth, nHeight);
  m_pPicture->Render(hMemDC, 0, 0, (int)(1.0*m*nWidth/n),
             (int)(1.0*m*nHeight/n),
    0, hmHeight, hmWidth, -hmHeight, &rect);
  m_pPicture->Release();
  m_pPicture = NULL;
  ::ReleaseDC(this->GetSafeHwnd(), hSelfDC);
  m_imageType = jpg;
  return TRUE;
}
//Zoom Image
HDC hSelfDC = ::GetDC(this->GetSafeHwnd());
HDC hMemDC2 = CreateCompatibleDC(hSelfDC);
HBITMAP bmpTemp = (HBITMAP)::SelectObject(hMemDC2, hBmpOriginal);
hMemDC = CreateCompatibleDC(hSelfDC);
BITMAP bm;
::GetObject(hBmpOriginal, sizeof(BITMAP), &bm);
hBmp = ::CreateCompatibleBitmap(hSelfDC,
         (int)(1.0*m*bm.bmWidth/n),
         (int)(1.0*m*bm.bmHeight/n));
hPrevBmp = (HBITMAP)::SelectObject(hMemDC, hBmp);
::ReleaseDC(this->GetSafeHwnd(), hSelfDC);
StretchBlt(hMemDC, 0, 0,(int)(1.0*m*bm.bmWidth/n),(int)
                             (1.0*m*bm.bmHeight/n), hMemDC2,
  0, 0, bm.bmWidth, bm.bmHeight, SRCCOPY );
::SelectObject(hMemDC2, bmpTemp);
::DeleteDC(hMemDC2);
DeleteObject(hBmpOriginal);

  //Create a Dib
  CBitmap bitmap;
  bitmap.Attach(hBmp);
  CDC dc;
  dc.Attach(hMemDC);
  BITMAP bm;
  // get bitmap information
  bitmap.GetObject(sizeof(bm),(LPSTR)&bm);

  int nBitCount = bm.bmBitsPixel;
  LPBITMAPINFOHEADER lpBMIH = (LPBITMAPINFOHEADER) new
  char[sizeof(BITMAPINFOHEADER) + sizeof(RGBQUAD) * 0];
  lpBMIH->biSize = sizeof(BITMAPINFOHEADER);
  lpBMIH->biWidth = bm.bmWidth;
  lpBMIH->biHeight = bm.bmHeight;
  lpBMIH->biPlanes = 1;
  lpBMIH->biBitCount = nBitCount;
  lpBMIH->biCompression = BI_RGB;
  lpBMIH->biSizeImage = 0;
  lpBMIH->biXPelsPerMeter = 0;
  lpBMIH->biYPelsPerMeter = 0;
  lpBMIH->biClrUsed = 0;
  lpBMIH->biClrImportant = 0;
  LPVOID lpImage = NULL;    // no data yet

  DWORD dwCount = ((DWORD) lpBMIH->biWidth
                * lpBMIH->biBitCount) / 32;
  if(((DWORD)lpBMIH->biWidth * lpBMIH->biBitCount) % 32) {
  dwCount++;
  }
  dwCount *= 4;
  dwCount = dwCount * lpBMIH->biHeight;
  lpImage = (LPBYTE)(LPVOID)::VirtualAlloc(NULL, dwCount,
  MEM_COMMIT, PAGE_READWRITE);
  ::ZeroMemory((LPVOID)lpImage,dwCount);

  // finally get the dib
  BOOL result = GetDIBits(dc.GetSafeHdc(),
                (HBITMAP)bitmap.GetSafeHandle(), 0L,
    (DWORD)bm.bmHeight, (LPBYTE)lpImage, (LPBITMAPINFO)lpBMIH,
                        (DWORD)DIB_RGB_COLORS);
  //Make Region
  hRegion= DIBRegion(lpBMIH, lpImage,m_clrBack, TRUE);
  delete lpBMIH;
  dc.Detach();
  bitmap.Detach();
}
  // If there was no problem getting the region, we make it
  // the current clipping region of the dialog's window
  if(hRegion)
 SetWindowRgn(hRegion,TRUE);

How to Use a Waitable Timer

To be frank, even after I read the MSDN and "Programming Application for MS Windows 2000" (1999, MS Press), it still took me quite some time to get used to the troublesome kernel object—waitable timer. Please Note: If you set an earlier time (than now) as the due time, the timer will be signaled immediately! In other words: if now is 11:00:00am, and you set a time from 10:59:00 (Due time) and signal every 5 minutes, the timer will be signaled immediately after the API call. Though it sounds reasonable, it means you have to be careful when setting the Due Time parameter in SetWaitableTimer API. Always comparing the due time with the current time will be a good habit. Following is a routine you may need to displace (move) time, it is useful to calculate a timer later or earlier than the given time:

BOOL MoveTime(CONST SYSTEMTIME *lpSystemTime1,
    // first system time [in]
                    SYSTEMTIME *lpSystemTime2,
    // second system time [out]
                    BOOL bPositive,
    // TRUE: Later Time; FALSE : Earlier Time
                    DWORD nDay, DWORD nHour, DWORD nMinute,
                                DWORD nSecond, DWORD nMilliSecond)
    //[in], Detailed Time Displacement
{
    //For your convenience: 1 second = 1,000 milliseconds
    //                               = 1,000,000 microseconds
    //                               = 1,000,000,000 nanoseconds.
    //The FILETIME structure is a 64-bit value representing the
    //number of 100-nanosecond intervals
    //since January 1, 1601 (UTC).

    LARGE_INTEGER n1, n2;
    n1.QuadPart = nDay;           n1.QuadPart *= 24;
    n1.QuadPart += nHour;         n1.QuadPart *= 60;
    n1.QuadPart += nMinute;       n1.QuadPart *= 60;
    n1.QuadPart += nSecond;       n1.QuadPart *= 1000;
    n1.QuadPart += nMilliSecond;  n1.QuadPart *= 10000;

    FILETIME ft;
    if(!::SystemTimeToFileTime(lpSystemTime1, &ft))
      return FALSE;

    n2.LowPart = ft.dwLowDateTime;
   n2.HighPart = ft.dwHighDateTime;

    if(bPositive)
      n2.QuadPart += n1.QuadPart;
    else
     n2.QuadPart -= n1.QuadPart;

    ft.dwLowDateTime = n2.LowPart;
    ft.dwHighDateTime = n2.HighPart;

    return ::FileTimeToSystemTime(&ft, lpSystemTime2);
}

Following is my background timer thread routine; the kill event will stop the thread and the refresh event will let the thread read a global structure (MMF or whatever shared data between GUI and thread) and refresh the waitable timer parameter:

DWORD WINAPI TimerThread(LPVOID lpParam)
{
  TimerPara* myPara = (TimerPara*)lpParam;
  HANDLE hKillEvent = myPara->hKillEvent;
  HANDLE hRefreshEvent = myPara->hRefreshEvent;

  LARGE_INTEGER li;
  // Create an auto-reset timer.
  HANDLE hWaitableTimer = ::CreateWaitableTimer(NULL, FALSE,
                                                NULL);
  // Timer unit is 100-nanoseconds.
  const int nTimerUnitsPerSecond = 10000000;

  HANDLE arrayEvent[3];
  arrayEvent[0] = hKillEvent;
  arrayEvent[1] = hRefreshEvent;
  arrayEvent[2] = hWaitableTimer;

  while(TRUE)
  {
    DWORD dwRet = ::WaitForMultipleObjects(3, arrayEvent, FALSE,
                                           INFINITE, FALSE);
    if(dwRet == WAIT_OBJECT_0) { ::CloseHandle(hWaitableTimer);
                                  return 0; }    //Kill Event
    else if(dwRet == WAIT_ABANDONED_0 ||
      dwRet == WAIT_ABANDONED_0 + 1 || dwRet
            == WAIT_ABANDONED_0 + 2)
    {
      ::CloseHandle(hWaitableTimer); return -1;
    }
    if(dwRet == WAIT_OBJECT_0 + 2)
    {
      //Do the thing you need to do; timer signalled
    }
    else if(dwRet == WAIT_OBJECT_0 + 1)    //Refresh Timer Setting
    {
      ::ResetEvent(hRefreshEvent);
      ::CancelWaitableTimer(hWaitableTimer);
      //Stop Possible Coming Timer

      //You can read from a global Variable or using MMF
      //Set New Due Time and Period of the timer
      SetWaitableTimer(hWaitableTimer, ......);
    }

  }
  return 0;
}

Known Limitations

Transparency on WinXP

It is strange on WinXP, when setting transparency of the dialog; the image will be badly painted like following figure, while in Win2K all is perfect. Anyone who solved this problem in WinXP, please comment. Thanx ahead :=)

Figure 5—Transparency Badly Painted on WinXP (While Win2K OK)

When Using JPEG images

Due to the nature of a JPEG, the encoding will lose some data; so, when we apply the transparent color to the image, usually in the perimeter, the pixel's color may not be what you think. The following left red rectangle, when saved as a bitmap, every pixel keeps its color; when saved as a JPEG, please note along the perimeter, the color changed. So, when I used JPEG to render a region, instead of using a clause like "if(r1== r2 && g1==g2 && b1 ==b2", I use "if (r1-r2)*(r1-r2) + (g1-g2)*(g1-g2) + (b1-b2)*(b1-b2) <= some threshold". Inevitably, this leads to other problems; for example, if some pixel inside the character has a similar color, it will be counted into the region even you need it. But, there seems no way to prevent this. So, please use bitmap files as much as possible.

Figure 6—Comparison of Bitmap and JPEG Image's Perimeter

This Program will NOT Shut Down Your Machine When a Screen Saver is Running

I have no intention of adding an complicated things, say, a NT Service, to this application to do a shutdown; it is just an accessory functionality, so just keep in mind it can NOT shut down your machine if the screen saver is running. So, if you want to shutdown your machine after three hours, disable your screen saver before you leave.

Acknowledgements

Thanks to the following article/code contributor on CodeGuru: Mr. Brent Corkum for his cool XP-style BCMenu, Mr. David Gallardo Llopis's article Technique to Create Dialogs from Images, Mr. Sam Hobbs for Processing Keyboard Messages, Mr. Dominik Filipp for How to dynamically show/hide the Taskbar application button (BTW, only a ModifyStyleEx(WS_EX_APPWINDOW, 0); will be enough in a dialog-based application).

Downloads

Download Demo Project Source (all the source code + exe) - 709 Kb
Download Demo Exe File Only (Exe Only, MFC library static linked) - 452 Kb

Version History

Version Release Date Features
1.0 Nov 12, 2002 First Version (BMP/JPEG File support, Tray Icon)
1.1 Nov 18, 2002 Shut Down support
1.2 Dec 14, 2002 Balloon Message Added
1.3 Xmas 2002 Finish This Article



Comments

  • yellow...

    Posted by Legacy on 03/07/2003 12:00am

    Originally posted by: xplong

    are u a nazi programmer?????? hahahahahahaha. Look your fucking face... if the IV reich will came, you will be the first on die.
    I think you have to dedicated to programming and not to sell us your fucked politic.

    Reply
  • SkinMagic SDK : I think it's the best solution for write skinnable application for C/C++ user!

    Posted by Legacy on 01/22/2003 12:00am

    Originally posted by: joe


    http://www.appspeed.com/

    SkinMagic SDK : I think it's the best solution for write skinnable application for C/C++ user!

    Reply
  • How to create region and alpha window??

    Posted by Legacy on 01/22/2003 12:00am

    Originally posted by: Joke

    How to create region and alpha window??

    Reply
  • Strange programname

    Posted by Legacy on 01/20/2003 12:00am

    Originally posted by: Dieter Hammer

    Hi, for me a program called "SS" with ascull as icon looks a little bit strange. But the work itself is fine!

    Reply
  • Transparency on WinXP solution

    Posted by Legacy on 01/20/2003 12:00am

    Originally posted by: Vitaly

    Hi,

    I know this problem. On windows 2000 and XP where it has transparency support, it is not compitible with custom-shape window. You only can use whether custom shape (custom window region), or transparency, not both at the same time!

    P.S. Looking for best tooltips ever, for VB, VC++, Delphi, ...any platform. Looking for a superb menu design, and/or UI in general? Visit http://www.tooltips.net

    Reply
  • Some bug?

    Posted by Legacy on 01/17/2003 12:00am

    Originally posted by: hiro

    Hi.

    I found one bug.

    Here is the step to happen bug ^^;

    1.Execute the Demo Binary.
    (The image apper on the Desktop)

    2.RightClick the image.
    (Menu appear)

    3.Select [Recent Image]->[mm.bmp]

    Note:
    In other environment, it may not be "mm.bmp".
    (choose the bottom one.)

    4.Bug happen.
    Lock Screen appear.
    This "Lock Screen" is the same as the Screen displayed
    when entered [Ctrl]+[Alt]+[Del] and Select "Lock Computer".

    My Environment is Windows2000 Pro (Japanese Edition, SP3).

    Thank you.

    Reply
Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • Learn How A Global Entertainment Company Saw a 448% ROI Every business today uses software to manage systems, deliver products, and empower employees to do their jobs. But software inevitably breaks, and when it does, businesses lose money -- in the form of dissatisfied customers, missed SLAs or lost productivity. PagerDuty, an operations performance platform, solves this problem by helping operations engineers and developers more effectively manage and resolve incidents across a company's global operations. …

  • Live Event Date: December 18, 2014 @ 2:00 p.m. ET / 11:00 a.m. PT The Internet of Things (IoT) incorporates physical devices into business processes using predictive analytics. While it relies heavily on existing Internet technologies, it differs by including physical devices, specialized protocols, physical analytics, and a unique partner network. To capture the real business value of IoT, the industry must move beyond customized projects to general patterns and platforms. Check out this upcoming webcast …

Most Popular Programming Stories

More for Developers

RSS Feeds