Qt QWebEngineView에서 QGraphicsEffect 사용 시 문제점 및 해결

Acccdang·2021년 4월 15일
0

Qt

목록 보기
1/1
post-thumbnail

🧑 주절 주절..

현 회사는 대학을 상대로 강의관리시스템(LMS) 솔루션을 운영하고 있는데, 이와 관련해서 사내 프로젝트로 데스크탑 응용 프로그램을 Qt Framework를 기반으로 C++ 언어 환경에서 개발하고 있다.

🤔 QWebEngineView를 사용하게 된 계기(?)

프로젝트를 진행하면서 로그인 기능을 구현해야 했는데, 구현하려하다 보니 아래와 같은 문제가 있었다.

  1. 대학마다 로그인이 동작하는 방식이 다 다르다.
- 대학측 데이터를 DB상에서 동기화 하는 경우
- SSO url call 후 return되는 결과값을 받는 경우
- 아예 SSO Page로 넘어가 로그인 후 return url로 우리쪽 sso 처리단으로 넘어가는 경우
  1. 그 중 SSO Page로 넘어가는 경우엔 따로 그 학교의 SSO Page를 호출해서 띄워줘야 하기 때문에, 웹뷰로 보여주는것이 필요했다.

이러한 이유에서 찾아보니 Qt에서 지원하는 QWebEngineView Class가 있었고, 이를 이용하면 될 것 같았다.

※ QWebEngineView의 자세한 내용은 Qt WebEngine Overview document 를 살펴보면 개략적인 구조를 이해할 수 있다.

🐱‍🏍 [To Do] 로그인 화면을 모두 웹뷰로 띄워볼까? 그런데..

앞선 이유 때문에 SSO Page를 직접 웹뷰로 띄워줘야 하면, 차라리 로그인 폼을 모두 jsp로 작성하고, 이걸 웹뷰로 보여주면 되지 않을까? 하는 생각이 들었다.
왜냐하면 그러면 따로 클라이언트와 서버와 직접적인 로그인 부분을 구현하지 않고, Login Form에서만 로그인 기능을 수행하면 되기 때문에 작업하기도 수월할 것 같았다. (물론 기존 솔루션에 SSO 처리 등 이미 잡혀있는 부분이 있기 때문에 구현하기 편리할 것 같기도 했다.)

물론 웹뷰 내 로그인 페이지에서 로그인이 모두 동작하고 나면 웹처럼 main form으로 redirect된다던지 하는게 아닌, Client가 이를 확인하고 그 후 동작을 Client에서 처리해줘야 한다.
이를 가능하게 해주는건 QWebChannelQWebChannel Javascript API를 통해 구현이 가능한데, 이 부분은 나중에 따로 포스팅을 해서 소개하도록 하겠다.

아무튼 QWebEngineView를 상속받은 내 Widget Class를 따로 구현해주고, 이걸 Qt의 .ui 파일을 이용해 Login Dialog에 컴포넌트로 추가한 후 promote 해줬다.

그런데 문제가 생겼다...

웹뷰를 로드해도 페이지가 뭐랄까, renewing이 되지 않는?.. 동작은 하는 것 같은데 scrolling을 하면 웹뷰가 다시 repaint가 되지 않았다. 그런데 이상한 건 focus를 다른 곳에 주었다가 다시 다이얼로그에 주게 되면 이전에 scrolling했던 게 잘 반영되어 있었다.

(스크롤 바를 누르고 내려도, 마우스로 스크롤 업/다운을 해도 마찬가지이다..)

살펴보니 나는 QDialog를 상속받은 Dialog Class를 구현한 뒤 사용하고 있었는데, frameless Dialog를 구현하기 위해 아래와 같이 세팅해놓았다.

// 기존 windowFlags에 frameless 속성 추가
this->setWindowFlags(this->windowFlags() | Qt::FramelessWindowHint);
// background 투명하게 설정
this->setAttribute(Qt::WA_TranslucentBackground);

// dialog frame이 없어짐에 따라 경계 구분이 안되는 것을 방지하기 위해, 그림자 효과를 추가
QGraphicsDropShadowEffect* dialogShadow = new QGrphicsDropShadowEffect(this);
dialogShadow->setBlurRadius(10);
dialogShadow->setColor(QColor(0, 0, 0, 60));
dialogShadow->setXOffset(0);
dialogShadow->setYOffset(0);
this->setGraphicsEffect(dialogShadow);

처음엔 Qt::FramelessWindowHintQt::WA_TranslucentBackground 의 문제인줄 알았으나, 확인해보니 QGraphicsDropShadowEffect 에서 dialogShadow->setBlurRadius(10); 부분이 문제였다. radius를 0으로 설정하고 테스트해보니 잘 된다.

dialogShadow->setBlurRadius(0);	// 이놈을 0으로 만들어 blur 효과를 없애버린다.

(정상적으로 되는게 확인된다.)

테스트 도중 잠깐 Scrolling이 되다가 안되는 경우를 발견해서, QWebEngineView에서 emit해주는 signals을 connect해봐서 테스트를 해보았다.

// QWebEngineView Class를 상속받은 Class의 생성자에서 호출하고 있기 때문에,
// signal 발생 객체는 this로 가정한다.
connect(this, &QWebEngineView::loadStarted, [&]() {
    /* load started */
    qDebug() << "WebEngineView :: load started"; 
    loadingWidget = new XVLoadingWidget(this); // custom loading indicator Widget..
    loadingwidget->show(); // webView가 loading에 들어가면 loading indicator를 발생시킨다.
};
connect(this, &QWebEngineView::loadProgress, [&](int progress) {
    /* load progress.. */
    // 이 쪽을 호출하고 있을 땐 scrolling에 문제가 없다.
    qDebug() << "WebEngineView :: load progress :: " << progress;
};
connect(this, &QWebEngineView::loadFinished, [&](bool ok) {
    /* load finished */
    // load가 끝나고 나면 scrolling이 동작하지 않는다.
    qDebug() << "WebEngineView :: load finished :: " << ok;
};
connect(this, &QWebEngineView::renderProcessTerminated, [&](QWebEnginePage::RenderProcessTerminationStatus terminationStatus, int exitCode) {
    /* load started */
    // 혹시 renderProcess 도중 문제가 생겨 terminated 된건 아닐까 싶어 살펴봤지만, 호출되지 않았다.
    // 생각해보면 scrolling만 안되지 동작은 하기 때문에... render 도중 실패한 건 아닌 듯 하다.
    qDebug() << "render process terminated..";
    qDebug() << "terminationStatus :: " << terminationStatus;
    qDebug() << "exitCode :: " << exitCode;
};
// 호출 결과

WebEngineView :: load started
WebEngineView :: load progress ::  0
WebEngineView :: load progress ::  23
WebEngineView :: load progress ::  46
WebEngineView :: load progress ::  52
WebEngineView :: load progress ::  70
WebEngineView :: load progress ::  80
WebEngineView :: load progress ::  100
WebEngineView :: load progress ::  100
WebEngineView :: load finished ::  true

// renderProcessTerminated는 호출되지 않았다.

(loadProgress signal이 호출되고 있는 동안엔 되다가, loadFinished signal이 호출되면 다시 멈춰버린다.)

😂 결론, 그리고 대처..

구글링을 좀 해보니, 나와 같은 문제를 겪은 문의글을 발견했다. (2020년 12월, 나름 따끈따끈한 질문글인데 아직 명확한 답변은 달리지 않았길래 나도 해결은 못했지만, 원인이라도 알리기 위해 댓글을 작성해줬다..)

뭐.. 결론은 QDialogQGraphicsDropShadowEffect->setBlurRadius(N)을 세팅하면 그 다이얼로그 안의 QWebEngineView 호출 시 렌더링 이슈가 있더라.. 정도만 확인할 수 있었다.

QGraphicsDropShadowEffect ClassQGraphicsEffect Class를 상속받고 있기 때문에, 그 밖의 다른 하위 클래스에도 이러한 문제가 있을지는 모르겠다. (더 테스트해보긴 시간이 없다...)

결국 이 부분을 해결하기 위해서는 그림자 처리를 빼면 되지만, 이러면 뒤 배경과 색이 겹칠 경우 경계 구분이 되지 않기 때문에 결국은 frameless dialog도 포기해야 한다.

프로젝트적 문제로 기존 frameless dialog를 다시 변경하긴 힘들어서, 현재 생각한 대안으로는 SSO Page를 호출할 때만 팝업 다이얼로그로 띄워 웹뷰로 보여주고, 해당 팝업 다이얼로그는 어쩔수 없이(...) frame이 있는 기본적인 다이얼로그로 띄워줘야 할 것 같다.


(2021/04/28 수정)

QGraphicsEffect Class와 QWebEngineView 의 렌더링 충돌 이슈에 대해서는 해결하진 못했지만, 우회하는 방법을 찾아 기록을 남긴다.

구글링하다 우연찮게 해결법을 기술한 글을 발견했다. (중국 사이트라 파파고로 열심히 번역해가며 봤다..)

결론부터 얘기하자면.. 아예 paintEvent가 발생할 때 마다 QPainter , QPainterPath 를 이용해 그림자를 그리는 방식으로 해결이 된다.

개략적인 코드는 아래와 같다.

// 생성자
XVDialog::XVDialog(QWidget* parent) 
	: QDialog(parent) {
    // frameless를 위해 선언
    this->setWindowFlags(this->windowFlags() | Qt::FramelessWindowHint);
    
    // 그림자 영역 부분을 그리기 위한 마진을 준다. 아래 그림자두께 변수와 같은 값
    this->setContentsMargins(20, 20, 20, 20);
    this->setAttribute(Qt::WA_TranslucentBackground);
    this->setModal(true);
    this->installEventFilter(this);
    
    // Dialog가 focus in/out 됐을 경우를 구분하기 위해 signal/slot connect.
    connect(qApp, &QguiApplication::applicationStateChanged, this, [&]() {
    	switch(state) {
        case Qt::Application::ApplicationActive:
        	alpha = 150;	// 멤버변수. 적당한 값을 세팅하자.
        	break;
        case Qt::Application::ApplicationInactive:
        	alpha = 100;	// inactive 됐을 때 더 연해져야 해서 active 보다 더 적게 세팅
        	break;
        default:
        	break;
        }
    });
}
// eventFilter를 재정의, paintEvent를 재정의해도 무방하다.
bool XVDialog::eventFilter(QObject* obj, QEvent* event) {
  if (event->type() == QEvent::Paint) {
    int shadowsWidth = 5;	// 그림자 영역의 두께, setContentsMargins
    int radius = 1;		// Dialog Window의 border radius 값이라고 생각하면 된다.
        			// 원하는 radius 값을 넣어주면 된다.
            
    // 위 본문에서는 fillPath로 배경 색을 다 칠해줬지만, 나는 margin값과 shadow width를 같게 두어서 처리했다.
    //QPainterPath path;	
    //path.setFillRule(Qt::WindingFill);
    //path.addRoundedRect(shadowsWidth, shadowsWidth, this->width() - (shadowsWidth*2), this->height() - (shadowsWidth*2), radius, radius);
    //painter.fillPath(path, QBrush(Qt::white));

    QPainter painter(this);
    painter.setRednerHint(QPainter::Antialiasing, true);
    QColor color(200, 200, 200, 10);	// 적당한 색상 세팅
    
    // 그림자 영역을 넓혀가며 그림자를 그린다. 넓혀질수록 alpha값을 조절하여 gradient 처리를 한다.
    for (int i = 0; i < shadowsWidth; i++) {
      QPainterPath path; // QPainterPath Class를 이용해 곡선을 그릴 수 있게끔 한다.
      path.setFillRule(Qt::WindingFill);
      path.addRoundedRect(shadowsWidth - i, shadowsWidth - i, this->width() - ((shadowsWidth - i) * 2), this->height() - ((shadowsWidth - i) * 2), radius + i, radius + i);

      // application의 state가 변경될 때 마다 paintEvent가 다시 호출되므로, 그때마다 적정한 alpha값으로 세팅한다.
      color.setAlpha(alpha - qSqrt(i) * 50);
      painter.setPen(color);
      painter.drawPath(path);
    }    
  }
  
  return QDialog::eventfilter(obj, event);
}

그러면 아래와 같이 결과가 나온다.

profile
개발이 취미가 되고픈 개발자

0개의 댓글