WinForms의 WebBrowser 컨트롤로 자동화를 하다 보면 페이지 안의 특정 요소가 화면 어디에 있는지 알아야 할 때가 있습니다. 버튼 위치에 맞춰 폼 쪽 컨트롤을 띄우거나, 해당 위치로 스크롤하거나, 좌표로 마우스 클릭을 흉내 낼 때가 그렇죠.

HtmlElement 에는 OffsetRectangle 속성이 있어서 Left, Top 을 바로 읽을 수 있습니다. 그런데 이 값은 문서 전체 기준이 아니라 offsetParent 기준의 상대 좌표입니다. 요소가 position 이 지정된 박스나 표 셀 안에 들어 있으면, 그 부모 안에서의 위치만 나오기 때문에 실제 위치보다 훨씬 작은 값이 나옵니다. 처음 써 보면 좌표가 이상하게 왼쪽 위로 몰리는 이유가 대부분 이것입니다.

광고

OffsetParent를 따라 올라가며 더하기

해결 방법은 간단합니다. 요소 자신의 오프셋에서 시작해서 OffsetParent 를 하나씩 거슬러 올라가며 각 부모의 오프셋을 더해 주면 됩니다. 더 이상 부모가 없을 때(null)까지 더하면 문서 기준 좌표가 됩니다. 자바스크립트에서 offsetLeft 를 누적해 절대 위치를 구하던 방식과 같은 원리입니다.

     public int getXoffset(HtmlElement el)
     {
         //get element pos
         int xPos = el.OffsetRectangle.Left;

         //get the parents pos
         HtmlElement tempEl = el.OffsetParent;
         while (tempEl != null)
         {
             xPos += tempEl.OffsetRectangle.Left;
             tempEl = tempEl.OffsetParent;
         }

         return xPos;
     }

     public int getYoffset(HtmlElement el)
     {
         //get element pos
         int yPos = el.OffsetRectangle.Top;

         //get the parents pos
         HtmlElement tempEl = el.OffsetParent;
         while (tempEl != null)
         {
             yPos += tempEl.OffsetRectangle.Top;
             tempEl = tempEl.OffsetParent;
         }

         return yPos;
     }

사용할 때는 webBrowser1.Document.GetElementById("아이디") 같은 식으로 요소를 얻어서 넘기면 됩니다. 두 함수 구조가 완전히 같아서, 필요하면 하나로 합쳐 Point 를 돌려주게 만들어도 됩니다.

화면 좌표로 쓸 때 주의할 점

이렇게 구한 값은 문서(페이지) 기준입니다. 페이지가 스크롤돼 있으면 지금 보이는 영역 기준 위치와는 스크롤 양만큼 차이가 납니다. 컨트롤 위의 실제 위치가 필요하면 Document.Body.ScrollTop(문서 모드에 따라서는 documentElement 쪽 값)을 빼야 하고, 거기서 다시 폼이나 화면 좌표로 바꾸려면 webBrowser1.PointToScreen 같은 변환을 한 번 더 거쳐야 합니다.

또 OffsetRectangle 은 페이지가 다 그려진 뒤에 의미 있는 값을 줍니다. DocumentCompleted 이벤트 이후에 호출해야 하고, 스크립트로 레이아웃이 나중에 바뀌는 페이지라면 그 뒤에 다시 계산해야 맞는 좌표가 나옵니다. iframe 안의 요소라면 프레임 문서 안에서의 좌표이므로 프레임 자체의 위치도 더해 줘야 합니다.

두 함수를 하나로 합쳐 Point로 받기

X와 Y를 따로 구하면 같은 부모 사슬을 두 번 도는 셈이라, 저는 한 번에 둘 다 더해서 Point 로 돌려주는 쪽을 씁니다. 반복을 요소 자신부터 시작하면 첫 줄을 따로 쓸 필요도 없습니다.

public Point GetDocumentPosition(HtmlElement el)
{
    int x = 0, y = 0;
    for (HtmlElement cur = el; cur != null; cur = cur.OffsetParent)
    {
        x += cur.OffsetRectangle.Left;
        y += cur.OffsetRectangle.Top;
    }
    return new Point(x, y);
}

GetElementById 는 해당 아이디가 없으면 null 을 돌려주니, 넘기기 전에 한 번 확인해야 NullReferenceException 을 피할 수 있습니다. 그리고 DocumentCompleted 는 페이지 안에 프레임이 있으면 프레임 수만큼 여러 번 불립니다. 이벤트 인자의 e.Url 이 webBrowser1.Url 과 같을 때만 계산하도록 걸러 두면 아직 덜 그려진 시점의 좌표를 쓰는 일이 줄어듭니다.

스크롤만 하려는 거라면

요소가 보이는 곳까지 화면을 옮기는 게 목적이라면 좌표를 직접 구할 필요가 없습니다. el.ScrollIntoView(true) 를 부르면 그 요소가 보이는 위치로 알아서 스크롤됩니다. 좌표 계산은 그 위치에 다른 컨트롤을 겹쳐 띄우거나 클릭 지점을 정해야 할 때만 꺼내 쓰는 편이 간단합니다.

참고로 WinForms WebBrowser 는 따로 설정하지 않으면 IE7 호환 모드로 페이지를 그립니다. 같은 페이지라도 최신 브라우저와 레이아웃이 달라 좌표가 어긋날 수 있으니, 브라우저 개발자 도구에서 본 값과 다르다고 바로 계산이 틀렸다고 보지 않는 게 좋습니다.