Okay so I managed to solve it. For future reference, here's what I did:
First, I tried to find a monospaced font, but I was unable to find any that were truly monospaced (had latin and chinese characters as the same width).
Next, I tried to center the text in the window. I was unable to do this until I realized that having Auto HScroll set to true (ES_AUTOHSCROLL defined for the rich edit control) caused setParaFormat to ignore me trying to center the text. After I disabled it and manually set the size of the drawable text area, I was able to center the text. Just in case anyone is curious, here's the code I used to set the width of the drawable area in the rich edit box:
CDC* pDC = GetDC();
long lw = 99999999;
SetTargetDevice(*GetDC(), lw);
I just set lw arbitrarily large so I could test to see if centering the text worked. It did not. As it turns out, when the rich edit control centers the text, it bases it off the draw width of the text, not off the number of characters. I assumed that since there were the same number of characters on either side of the query string then that would cause the string to be centered, but this was not the case.
The final solution I tried was the one suggested by ymett. After some tweaking I came up with a function called alignText() that's called after all the text has been inserted into the rich edit control. Here's the function (Note: each line in the control had tabs inserted before this function was called: one at the beginning of the line and one after the query string e.g. "string1-query-string2" becomes "\tstring1-query\t-string2")
void CFormViewConcordanceRichEditCtrl::alignText()
{
long maxSize = 0;
CFont font;
LOGFONT lf = {0};
CHARFORMAT cf = {0};
this->GetDefaultCharFormat(cf);
//convert a CHARFORMAT struct into a LOGFONT struct
if (cf.dwEffects & CFE_BOLD)
lf.lfWeight = FW_BOLD;
else
lf.lfWeight = FW_NORMAL;
if (cf.dwEffects & CFE_ITALIC)
lf.lfItalic = true;
if (cf.dwEffects & CFE_UNDERLINE)
lf.lfUnderline = true;
if (cf.dwEffects & CFE_STRIKEOUT)
lf.lfStrikeOut = true;
lf.lfHeight = cf.yHeight;
_stprintf(lf.lfFaceName, _T("%s"), cf.szFaceName);
lf.lfCharSet = DEFAULT_CHARSET;
lf.lfOutPrecision = OUT_DEFAULT_PRECIS;
font.CreateFontIndirect(&lf);
//create a display context
CClientDC dc(this);
dc.SetMapMode(MM_TWIPS);
CFont *pOldFont = dc.SelectObject(&font);
//find the line that as the longest preceding string when drawn
for(int i = 0; i < m_pParent->m_nDataArray; i++)
{
CString text = m_pParent->DataArray[i].text.Left(BUFFER_LENGTH + m_pParent->generateText.GetLength());
text.Replace(_T("\r"), _T(" "));
text.Replace(_T("\n"), _T(" "));
text.Replace(_T("\t"), _T(" "));
CRect rc(0,0,0,0);
dc.DrawText(text, &rc, DT_CALCRECT);
int width = 1.0*cf.yHeight/fabs((double)rc.bottom - rc.top)*(rc.right - rc.left);
width = dc.GetTextExtent(text).cx;
if(width > maxSize)
maxSize = width;
}
dc.SelectObject(pOldFont);
//this calulates where to place the first tab. The 0.8 is a rought constant calculated by guess & check, it may be innacurate.
long tab = maxSize*0.8;
PARAFORMAT pf;
pf.cbSize = sizeof(PARAFORMAT);
pf.dwMask = PFM_TABSTOPS;
pf.cTabCount = 2;
pf.rgxTabs[0] = tab + (2 << 24); //make the first tab right-aligned
pf.rgxTabs[1] = tab + 25;
//this is to preserve the user's selection and scroll positions when the selection is changed
int vScroll = GetScrollPos(SB_VERT);
int hScroll = GetScrollPos(SB_HORZ);
long spos, epos;
GetSel(spos, epos);
//select all the text
SetSel(0, -1);
//this call is very important, but I'm not sure why
::SendMessage(GetSafeHwnd(), EM_SETTYPOGRAPHYOPTIONS, TO_ADVANCEDTYPOGRAPHY, TO_ADVANCEDTYPOGRAPHY);
this->SetParaFormat(pf);
//now reset the user's selection and scroll positions
SetSel(spos, epos);
::SendMessage(GetSafeHwnd(),
WM_HSCROLL,
(WPARAM) ((hScroll) << 16) + SB_THUMBPOSITION,
(LPARAM) NULL);
::SendMessage(GetSafeHwnd(),
WM_VSCROLL,
(WPARAM) ((vScroll) << 16) + SB_THUMBPOSITION,
(LPARAM) NULL);
}
Essentially what this function does is make the first tab stop right-aligned and set it at some point x to the right in the control. It then makes the second tab stop a small distance to the right of that, and makes it left-aligned. So all the text from the beginning of each line to the end of the query string (from the first \t to the second \t) is pushed toward the right against the first tab stop, and all the remaining text is pushed toward the left against the second tab stop, causing the query string to be aligned between all the lines in the control. The first part of the function finds out x (by finding out how long each line will be drawn and taking the max) and the second part sets the tab stops.
Thanks again to ymett for the solution.